feat: add session download tracking to assetDownloadStore (#8213)

## Summary

Add session download tracking to track which assets were downloaded
during the current session. This enables UI features like:
- Badge count on "Imported" nav showing newly downloaded assets
- Visual indicator on asset cards for recently downloaded items

## Changes

- Add `acknowledged` flag to `AssetDownload` interface
- Add `unacknowledgedDownloads` computed for filtering
- Add `sessionDownloadCount` computed for badge display
- Add `isDownloadedThisSession(identifier)` to check individual assets
- Add `acknowledgeDownload(identifier)` to mark assets as seen

## Testing

- 6 new unit tests covering all session tracking functionality
- Run: `pnpm test:unit -- src/stores/assetDownloadStore.test.ts`

## Related

- Part of Asset Browser improvements (#8090)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8213-feat-add-session-download-tracking-to-assetDownloadStore-2ef6d73d365081538045e8544d26bafa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-21 16:32:30 -08:00
committed by GitHub
parent d12c6d7814
commit f1d1747582
6 changed files with 272 additions and 86 deletions

View File

@@ -2,6 +2,7 @@ import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import { useFuse } from '@vueuse/integrations/useFuse'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import { storeToRefs } from 'pinia'
import { d, t } from '@/i18n'
import type { FilterState } from '@/platform/assets/components/AssetFilterBar.vue'
@@ -10,9 +11,13 @@ import {
getAssetBaseModel,
getAssetDescription
} from '@/platform/assets/utils/assetMetadataUtils'
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
import type { NavGroupData, NavItemData } from '@/types/navTypes'
export type OwnershipOption = 'all' | 'my-models' | 'public-models'
type NavId = 'all' | 'imported' | (string & {})
function filterByCategory(category: string) {
return (asset: AssetItem) => {
if (category === 'all') return true
@@ -81,9 +86,12 @@ export function useAssetBrowser(
assetsSource: Ref<AssetItem[] | undefined> = ref<AssetItem[] | undefined>([])
) {
const assets = computed<AssetItem[]>(() => assetsSource.value ?? [])
const assetDownloadStore = useAssetDownloadStore()
const { sessionDownloadCount } = storeToRefs(assetDownloadStore)
// State
const searchQuery = ref('')
const selectedCategory = ref('all')
const selectedNavItem = ref<NavId>('all')
const filters = ref<FilterState>({
sortBy: 'recent',
fileFormats: [],
@@ -91,6 +99,21 @@ export function useAssetBrowser(
ownership: 'all'
})
const selectedOwnership = computed<OwnershipOption>(() => {
if (selectedNavItem.value === 'imported') return 'my-models'
return 'all'
})
const selectedCategory = computed(() => {
if (
selectedNavItem.value === 'all' ||
selectedNavItem.value === 'imported'
) {
return 'all'
}
return selectedNavItem.value
})
// Transform API asset to display asset
function transformAssetForDisplay(asset: AssetItem): AssetDisplayItem {
// Extract description from metadata or create from tags
@@ -136,39 +159,69 @@ export function useAssetBrowser(
}
}
const availableCategories = computed(() => {
const typeCategories = computed<NavItemData[]>(() => {
const categories = assets.value
.filter((asset) => asset.tags[0] === 'models')
.map((asset) => asset.tags[1])
.filter((tag): tag is string => typeof tag === 'string' && tag.length > 0)
.map((tag) => tag.split('/')[0]) // Extract top-level folder name
.map((tag) => tag.split('/')[0])
const uniqueCategories = Array.from(new Set(categories))
return Array.from(new Set(categories))
.sort()
.map((category) => ({
id: category,
label: category.charAt(0).toUpperCase() + category.slice(1),
icon: 'icon-[lucide--package]'
icon: 'icon-[lucide--folder]'
}))
})
return [
const navItems = computed<(NavItemData | NavGroupData)[]>(() => {
const quickFilters: NavItemData[] = [
{
id: 'all',
label: t('assetBrowser.allModels'),
icon: 'icon-[lucide--folder]'
icon: 'icon-[lucide--list]'
},
...uniqueCategories
{
id: 'imported',
label: t('assetBrowser.imported'),
icon: 'icon-[lucide--folder-input]',
badge:
sessionDownloadCount.value > 0
? sessionDownloadCount.value
: undefined
}
]
if (typeCategories.value.length === 0) {
return quickFilters
}
return [
...quickFilters,
{
title: t('assetBrowser.byType'),
items: typeCategories.value,
collapsible: false
}
]
})
// Compute content title from selected category
const isImportedSelected = computed(
() => selectedNavItem.value === 'imported'
)
// Compute content title from selected nav item
const contentTitle = computed(() => {
if (selectedCategory.value === 'all') {
if (selectedNavItem.value === 'all') {
return t('assetBrowser.allModels')
}
if (selectedNavItem.value === 'imported') {
return t('assetBrowser.imported')
}
const category = availableCategories.value.find(
(cat) => cat.id === selectedCategory.value
const category = typeCategories.value.find(
(cat) => cat.id === selectedNavItem.value
)
return category?.label || t('assetBrowser.assets')
})
@@ -205,7 +258,7 @@ export function useAssetBrowser(
const filtered = searchFiltered.value
.filter(filterByFileFormats(filters.value.fileFormats))
.filter(filterByBaseModels(filters.value.baseModels))
.filter(filterByOwnership(filters.value.ownership))
.filter(filterByOwnership(selectedOwnership.value))
const sortedAssets = [...filtered]
sortedAssets.sort((a, b) => {
@@ -234,11 +287,13 @@ export function useAssetBrowser(
return {
searchQuery,
selectedNavItem,
selectedCategory,
availableCategories,
navItems,
contentTitle,
categoryFilteredAssets,
filteredAssets,
isImportedSelected,
updateFilters
}
}