diff --git a/src/platform/assets/components/AssetFilterBar.vue b/src/platform/assets/components/AssetFilterBar.vue index 7e83c0e8c7..712639824e 100644 --- a/src/platform/assets/components/AssetFilterBar.vue +++ b/src/platform/assets/components/AssetFilterBar.vue @@ -9,7 +9,7 @@ > () -const fileFormats = ref([]) -const baseModels = ref([]) +const selectedFileFormats = ref([]) +const selectedBaseModels = ref([]) const sortBy = ref('recent') const ownership = ref('all') const { availableFileFormats, availableBaseModels, ownershipOptions } = useAssetFilterOptions(() => assets) +// Only show selected items that exist in the current scope +const activeFileFormatObjects = computed({ + get() { + return selectedFileFormats.value.filter((opt) => + availableFileFormats.value.some((a) => a.value === opt.value) + ) + }, + set(value: SelectOption[]) { + selectedFileFormats.value = value + } +}) + +const activeBaseModelObjects = computed({ + get() { + return selectedBaseModels.value.filter((opt) => + availableBaseModels.value.some((a) => a.value === opt.value) + ) + }, + set(value: SelectOption[]) { + selectedBaseModels.value = value + } +}) + const emit = defineEmits<{ filterChange: [filters: AssetFilterState] }>() function handleFilterChange() { emit('filterChange', { - fileFormats: fileFormats.value.map((option: SelectOption) => option.value), - baseModels: baseModels.value.map((option: SelectOption) => option.value), + fileFormats: activeFileFormatObjects.value.map((opt) => opt.value), + baseModels: activeBaseModelObjects.value.map((opt) => opt.value), sortBy: sortBy.value, ownership: ownership.value }) diff --git a/src/platform/assets/composables/useAssetBrowser.test.ts b/src/platform/assets/composables/useAssetBrowser.test.ts index 5a067527b8..498000cc1f 100644 --- a/src/platform/assets/composables/useAssetBrowser.test.ts +++ b/src/platform/assets/composables/useAssetBrowser.test.ts @@ -5,6 +5,12 @@ import { nextTick, ref } from 'vue' import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => key + }) +})) + vi.mock('@/i18n', () => ({ t: (key: string) => { const translations: Record = { @@ -736,6 +742,90 @@ describe('useAssetBrowser', () => { expect(contentTitle.value).toBe('Assets') }) + it('ignores stale file format filter when navigating to category without that format', async () => { + const assets = [ + createApiAsset({ + id: 'ckpt-safetensors', + name: 'model.safetensors', + tags: ['models', 'checkpoints'] + }), + createApiAsset({ + id: 'lora-pt', + name: 'lora.pt', + tags: ['models', 'loras'] + }), + createApiAsset({ + id: 'lora-pt-2', + name: 'lora2.pt', + tags: ['models', 'loras'] + }) + ] + + const { selectedNavItem, updateFilters, filteredAssets } = + useAssetBrowser(ref(assets)) + + // Select safetensors filter while viewing checkpoints + selectedNavItem.value = 'checkpoints' + updateFilters({ + sortBy: 'recent', + fileFormats: ['safetensors'], + baseModels: [], + ownership: 'all' + }) + await nextTick() + + expect(filteredAssets.value).toHaveLength(1) + expect(filteredAssets.value[0].id).toBe('ckpt-safetensors') + + // Navigate to loras category which has no .safetensors files + selectedNavItem.value = 'loras' + await nextTick() + + // Should show all loras, not empty (stale filter should be ignored) + expect(filteredAssets.value).toHaveLength(2) + }) + + it('ignores stale base model filter when navigating to category without that model', async () => { + const assets = [ + createApiAsset({ + id: 'ckpt-sdxl', + name: 'model.safetensors', + tags: ['models', 'checkpoints'], + user_metadata: { base_model: 'SDXL' } + }), + createApiAsset({ + id: 'lora-sd15', + name: 'lora.pt', + tags: ['models', 'loras'], + user_metadata: { base_model: 'SD1.5' } + }) + ] + + const { selectedNavItem, updateFilters, filteredAssets } = + useAssetBrowser(ref(assets)) + + // Select SDXL base model filter while viewing checkpoints + selectedNavItem.value = 'checkpoints' + updateFilters({ + sortBy: 'recent', + fileFormats: [], + baseModels: ['SDXL'], + ownership: 'all' + }) + await nextTick() + + expect(filteredAssets.value).toHaveLength(1) + expect(filteredAssets.value[0].id).toBe('ckpt-sdxl') + + // Navigate to loras which has no SDXL models + selectedNavItem.value = 'loras' + await nextTick() + + // Should show all loras, not empty + expect(filteredAssets.value).toHaveLength(1) + expect(filteredAssets.value[0].id).toBe('lora-sd15') + }) + it('groups models by top-level folder name', () => { const assets = [ createApiAsset({ diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts index 773a53ffe9..448a89b434 100644 --- a/src/platform/assets/composables/useAssetBrowser.ts +++ b/src/platform/assets/composables/useAssetBrowser.ts @@ -10,6 +10,7 @@ import type { OwnershipOption } from '@/platform/assets/types/filterTypes' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions' import { filterByBaseModels, filterByCategory, @@ -192,6 +193,22 @@ export function useAssetBrowser( return assets.value.filter(filterByCategory(selectedCategory.value)) }) + const { availableFileFormats, availableBaseModels } = useAssetFilterOptions( + categoryFilteredAssets + ) + + const activeFileFormats = computed(() => + filters.value.fileFormats.filter((f) => + availableFileFormats.value.some((opt) => opt.value === f) + ) + ) + + const activeBaseModels = computed(() => + filters.value.baseModels.filter((m) => + availableBaseModels.value.some((opt) => opt.value === m) + ) + ) + const fuseOptions: UseFuseOptions = { fuseOptions: { keys: [ @@ -223,8 +240,8 @@ export function useAssetBrowser( const filteredAssets = computed(() => { const filtered = searchFiltered.value - .filter(filterByFileFormats(filters.value.fileFormats)) - .filter(filterByBaseModels(filters.value.baseModels)) + .filter(filterByFileFormats(activeFileFormats.value)) + .filter(filterByBaseModels(activeBaseModels.value)) .filter(filterByOwnership(selectedOwnership.value)) const sortedAssets = sortAssets(filtered, filters.value.sortBy)