Full Asset Selection Experience (Assets API) (#5900)

## Summary

Full Integration of Asset Browsing and Selection when Assets API is
enabled.

## Changes

1. Replace Model Left Side Tab with experience
2. Configurable titles for the Asset Browser Modal
3. Refactors to simplify callback code
4. Refactor to make modal filters reactive (they change their values
based on assets displayed)
5. Add `browse()` mode with ability to create node directly from the
Asset Browser Modal (in `browse()` mode)

## Screenshots

Demo of many different types of Nodes getting configured by the Modal



https://github.com/user-attachments/assets/34f9c964-cdf2-4c5d-86a9-a8e7126a7de9

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5900-Feat-asset-selection-cloud-integration-2816d73d365081ccb4aeecdc14b0e5d3)
by [Unito](https://www.unito.io)
This commit is contained in:
Arjan Singh
2025-10-03 20:34:59 -07:00
committed by GitHub
parent 661885f5e5
commit abf2b3b980
22 changed files with 1452 additions and 554 deletions

View File

@@ -1,47 +1,54 @@
import { t } from '@/i18n'
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
interface AssetBrowserDialogProps {
interface ShowOptions {
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
nodeType: string
/** Widget input name (e.g., 'ckpt_name') */
inputName: string
/** Current selected asset value */
currentValue?: string
/**
* Callback for when an asset is selected
* @param {string} filename - The validated filename from user_metadata.filename
*/
onAssetSelected?: (filename: string) => void
onAssetSelected?: (asset: AssetItem) => void
}
interface BrowseOptions {
/** Asset type tag to filter by (e.g., 'models') */
assetType: string
/** Custom modal title (optional) */
title?: string
/** Called when asset selected */
onAssetSelected?: (asset: AssetItem) => void
}
const dialogComponentProps: DialogComponentProps = {
headless: true,
modal: true,
closable: true,
pt: {
root: {
class: 'rounded-2xl overflow-hidden asset-browser-dialog'
},
header: {
class: '!p-0 hidden'
},
content: {
class: '!p-0 !m-0 h-full w-full'
}
}
} as const
export const useAssetBrowserDialog = () => {
const dialogStore = useDialogStore()
const dialogKey = 'global-asset-browser'
async function show(props: AssetBrowserDialogProps) {
const handleAssetSelected = (filename: string) => {
props.onAssetSelected?.(filename)
async function show(props: ShowOptions) {
const handleAssetSelected = (asset: AssetItem) => {
props.onAssetSelected?.(asset)
dialogStore.closeDialog({ key: dialogKey })
}
const dialogComponentProps: DialogComponentProps = {
headless: true,
modal: true,
closable: true,
pt: {
root: {
class: 'rounded-2xl overflow-hidden asset-browser-dialog'
},
header: {
class: '!p-0 hidden'
},
content: {
class: '!p-0 !m-0 h-full w-full'
}
}
}
const assets: AssetItem[] = await assetService
.getAssetsForNodeType(props.nodeType)
@@ -54,6 +61,22 @@ export const useAssetBrowserDialog = () => {
return []
})
// Extract node type category from first asset's tags (e.g., "loras", "checkpoints")
// Tags are ordered: ["models", "loras"] so take the second tag
const nodeTypeCategory =
assets[0]?.tags?.find((tag) => tag !== 'models') ?? 'models'
const acronyms = new Set(['VAE', 'CLIP', 'GLIGEN'])
const categoryLabel = nodeTypeCategory
.split('_')
.map((word) => {
const uc = word.toUpperCase()
return acronyms.has(uc) ? uc : word
})
.join(' ')
const title = t('assetBrowser.allCategory', { category: categoryLabel })
dialogStore.showDialog({
key: dialogKey,
component: AssetBrowserModal,
@@ -62,6 +85,7 @@ export const useAssetBrowserDialog = () => {
inputName: props.inputName,
currentValue: props.currentValue,
assets,
title,
onSelect: handleAssetSelected,
onClose: () => dialogStore.closeDialog({ key: dialogKey })
},
@@ -69,5 +93,38 @@ export const useAssetBrowserDialog = () => {
})
}
return { show }
async function browse(options: BrowseOptions): Promise<void> {
const handleAssetSelected = (asset: AssetItem) => {
options.onAssetSelected?.(asset)
dialogStore.closeDialog({ key: dialogKey })
}
const assets = await assetService
.getAssetsByTag(options.assetType)
.catch((error) => {
console.error(
'Failed to fetch assets for tag:',
options.assetType,
error
)
return []
})
dialogStore.showDialog({
key: dialogKey,
component: AssetBrowserModal,
props: {
nodeType: undefined,
inputName: undefined,
assets,
showLeftPanel: true,
title: options.title,
onSelect: handleAssetSelected,
onClose: () => dialogStore.closeDialog({ key: dialogKey })
},
dialogComponentProps
})
}
return { show, browse }
}