Asset Browser Design Review + Filters (#5737)

## Summary

Fixed design feedback and wired up the filter controls.

## Review Focus

Design Feedback:
-
[4872888](48728881af)
-
[9a0b63e](9a0b63edce)

Filters Hookup:
-
[07f22f8](07f22f8074)

Misc (can focus less on):
- claude guidance:
[23e6fa9](23e6fa9723)
- test helpers:
[7801ed9](7801ed9e28)

## Screenshots (if applicable)
<img width="1534" height="1175" alt="Screenshot 2025-09-23 at 1 03
12 PM"
src="https://github.com/user-attachments/assets/d82088e4-7d72-4c6f-904e-5180774d64a5"
/>

<img width="1794" height="793" alt="Screenshot 2025-09-23 at 1 03 22 PM"
src="https://github.com/user-attachments/assets/56eac2ba-5ecc-4a20-843f-ce683dea668c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5737-Asset-Browser-Design-Review-Filters-2776d73d3650813e890bd16fa6a0433f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Arjan Singh
2025-09-25 11:17:26 -07:00
committed by GitHub
parent a0c06bd723
commit 13ce23399c
17 changed files with 473 additions and 130 deletions

View File

@@ -1,6 +1,7 @@
import { computed, ref } from 'vue'
import { d, t } from '@/i18n'
import type { FilterState } from '@/platform/assets/components/AssetFilterBar.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetFilenameSchema } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
@@ -10,6 +11,43 @@ import {
} from '@/platform/assets/utils/assetMetadataUtils'
import { formatSize } from '@/utils/formatUtil'
function filterByCategory(category: string) {
return (asset: AssetItem) => {
return category === 'all' || asset.tags.includes(category)
}
}
function filterByQuery(query: string) {
return (asset: AssetItem) => {
if (!query) return true
const lowerQuery = query.toLowerCase()
const description = getAssetDescription(asset)
return (
asset.name.toLowerCase().includes(lowerQuery) ||
(description && description.toLowerCase().includes(lowerQuery)) ||
asset.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
)
}
}
function filterByFileFormats(formats: string[]) {
return (asset: AssetItem) => {
if (formats.length === 0) return true
const formatSet = new Set(formats)
const extension = asset.name.split('.').pop()?.toLowerCase()
return extension ? formatSet.has(extension) : false
}
}
function filterByBaseModels(models: string[]) {
return (asset: AssetItem) => {
if (models.length === 0) return true
const modelSet = new Set(models)
const baseModel = getAssetBaseModel(asset)
return baseModel ? modelSet.has(baseModel) : false
}
}
type AssetBadge = {
label: string
type: 'type' | 'base' | 'size'
@@ -35,7 +73,11 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
// State
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('name')
const filters = ref<FilterState>({
sortBy: 'name-asc',
fileFormats: [],
baseModels: []
})
// Transform API asset to display asset
function transformAssetForDisplay(asset: AssetItem): AssetDisplayItem {
@@ -84,16 +126,18 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
}
}
// Extract available categories from assets
const availableCategories = computed(() => {
const categorySet = new Set<string>()
const categories = assets
.filter((asset) => asset.tags[0] === 'models' && asset.tags[1])
.map((asset) => asset.tags[1])
assets.forEach((asset) => {
// Second tag is the category (after 'models' root tag)
if (asset.tags.length > 1 && asset.tags[0] === 'models') {
categorySet.add(asset.tags[1])
}
})
const uniqueCategories = Array.from(new Set(categories))
.sort()
.map((category) => ({
id: category,
label: category.charAt(0).toUpperCase() + category.slice(1),
icon: 'icon-[lucide--package]'
}))
return [
{
@@ -101,13 +145,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
label: t('assetBrowser.allModels'),
icon: 'icon-[lucide--folder]'
},
...Array.from(categorySet)
.sort()
.map((category) => ({
id: category,
label: category.charAt(0).toUpperCase() + category.slice(1),
icon: 'icon-[lucide--package]'
}))
...uniqueCategories
]
})
@@ -123,37 +161,25 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
return category?.label || t('assetBrowser.assets')
})
// Filter functions
const filterByCategory = (category: string) => (asset: AssetItem) => {
if (category === 'all') return true
return asset.tags.includes(category)
}
const filterByQuery = (query: string) => (asset: AssetItem) => {
if (!query) return true
const lowerQuery = query.toLowerCase()
const description = getAssetDescription(asset)
return (
asset.name.toLowerCase().includes(lowerQuery) ||
(description && description.toLowerCase().includes(lowerQuery)) ||
asset.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
)
}
// Computed filtered and transformed assets
const filteredAssets = computed(() => {
const filtered = assets
.filter(filterByCategory(selectedCategory.value))
.filter(filterByQuery(searchQuery.value))
.filter(filterByFileFormats(filters.value.fileFormats))
.filter(filterByBaseModels(filters.value.baseModels))
// Sort assets
filtered.sort((a, b) => {
switch (sortBy.value) {
case 'date':
switch (filters.value.sortBy) {
case 'name-desc':
return b.name.localeCompare(a.name)
case 'recent':
return (
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
case 'name':
case 'popular':
return a.name.localeCompare(b.name)
case 'name-asc':
default:
return a.name.localeCompare(b.name)
}
@@ -200,18 +226,17 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
}
}
function updateFilters(newFilters: FilterState) {
filters.value = { ...newFilters }
}
return {
// State
searchQuery,
selectedCategory,
sortBy,
// Computed
availableCategories,
contentTitle,
filteredAssets,
// Actions
selectAssetWithCallback
selectAssetWithCallback,
updateFilters
}
}