Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 37 additions & 45 deletions src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,8 @@
</template>

<script setup lang="ts">
import {
breakpointsTailwind,
useAsyncState,
useBreakpoints
} from '@vueuse/core'
import { computed, provide, watch } from 'vue'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { computed, provide } from 'vue'
import { useI18n } from 'vue-i18n'

import SearchBox from '@/components/common/SearchBox.vue'
Expand All @@ -81,68 +77,68 @@ import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBro
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel'
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
import { useAssetsStore } from '@/stores/assetsStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
import { OnCloseKey } from '@/types/widgetTypes'

const { t } = useI18n()
const assetStore = useAssetsStore()
const modelToNodeStore = useModelToNodeStore()
const breakpoints = useBreakpoints(breakpointsTailwind)

const props = defineProps<{
nodeType?: string
assetType?: string
onSelect?: (asset: AssetItem) => void
onClose?: () => void
showLeftPanel?: boolean
title?: string
assetType?: string
}>()

const { t } = useI18n()

const emit = defineEmits<{
'asset-select': [asset: AssetDisplayItem]
close: []
}>()

const breakpoints = useBreakpoints(breakpointsTailwind)

provide(OnCloseKey, props.onClose ?? (() => {}))

const fetchAssets = async () => {
// Compute the cache key based on nodeType or assetType
const cacheKey = computed(() => {
if (props.nodeType) return props.nodeType
if (props.assetType) return `tag:${props.assetType}`
return ''
})

// Read directly from store cache - reactive to any store updates
const fetchedAssets = computed(
() => assetStore.modelAssetsByNodeType.get(cacheKey.value) ?? []
)

const isStoreLoading = computed(
() => assetStore.modelLoadingByNodeType.get(cacheKey.value) ?? false
)

// Only show loading spinner when loading AND no cached data
const isLoading = computed(
() => isStoreLoading.value && fetchedAssets.value.length === 0
)

async function refreshAssets(): Promise<AssetItem[]> {
if (props.nodeType) {
return (await assetService.getAssetsForNodeType(props.nodeType)) ?? []
return await assetStore.updateModelsForNodeType(props.nodeType)
}

if (props.assetType) {
return (await assetService.getAssetsByTag(props.assetType)) ?? []
return await assetStore.updateModelsForTag(props.assetType)
}

return []
}

const {
state: fetchedAssets,
isLoading,
execute
} = useAsyncState<AssetItem[]>(fetchAssets, [], { immediate: false })

watch(
() => [props.nodeType, props.assetType],
async () => {
await execute()
},
{ immediate: true }
)

const assetDownloadStore = useAssetDownloadStore()
// Trigger background refresh on mount
void refreshAssets()

watch(
() => assetDownloadStore.hasActiveDownloads,
async (currentlyActive, previouslyActive) => {
if (previouslyActive && !currentlyActive) {
await execute()
}
}
)
const { isUploadButtonEnabled, showUploadDialog } =
useModelUpload(refreshAssets)

const {
searchQuery,
Expand All @@ -153,8 +149,6 @@ const {
updateFilters
} = useAssetBrowser(fetchedAssets)

const modelToNodeStore = useModelToNodeStore()

const primaryCategoryTag = computed(() => {
const assets = fetchedAssets.value ?? []
const tagFromAssets = assets
Expand Down Expand Up @@ -202,6 +196,4 @@ function handleAssetSelectAndEmit(asset: AssetDisplayItem) {
// It handles the appropriate transformation (filename extraction or full asset)
props.onSelect?.(asset)
}

const { isUploadButtonEnabled, showUploadDialog } = useModelUpload(execute)
</script>
6 changes: 2 additions & 4 deletions src/platform/assets/composables/useModelUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import UploadModelDialog from '@/platform/assets/components/UploadModelDialog.vu
import UploadModelDialogHeader from '@/platform/assets/components/UploadModelDialogHeader.vue'
import UploadModelUpgradeModal from '@/platform/assets/components/UploadModelUpgradeModal.vue'
import UploadModelUpgradeModalHeader from '@/platform/assets/components/UploadModelUpgradeModalHeader.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useDialogStore } from '@/stores/dialogStore'
import type { UseAsyncStateReturn } from '@vueuse/core'
import { computed } from 'vue'

export function useModelUpload(
execute?: UseAsyncStateReturn<AssetItem[], [], true>['execute']
onUploadSuccess?: () => Promise<unknown> | void
) {
const dialogStore = useDialogStore()
const { flags } = useFeatureFlags()
Expand Down Expand Up @@ -37,7 +35,7 @@ export function useModelUpload(
component: UploadModelDialog,
props: {
onUploadSuccess: async () => {
await execute?.()
await onUploadSuccess?.()
}
},
dialogComponentProps: {
Expand Down
98 changes: 62 additions & 36 deletions src/stores/assetsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useAsyncState } from '@vueuse/core'
import { isEqual } from 'es-toolkit'
import { defineStore } from 'pinia'
import { computed, shallowReactive, ref, watch } from 'vue'
import {
Expand Down Expand Up @@ -280,74 +281,98 @@ export const useAssetsStore = defineStore('assets', () => {
)

/**
* Fetch and cache model assets for a specific node type
* Uses VueUse's useAsyncState for automatic loading/error tracking
* @param nodeType The node type to fetch assets for (e.g., 'CheckpointLoaderSimple')
* @returns Promise resolving to the fetched assets
* Internal helper to fetch and cache assets with a given key and fetcher
*/
async function updateModelsForNodeType(
nodeType: string
async function updateModelsForKey(
key: string,
fetcher: () => Promise<AssetItem[]>
): Promise<AssetItem[]> {
if (!stateByNodeType.has(nodeType)) {
if (!stateByNodeType.has(key)) {
stateByNodeType.set(
nodeType,
useAsyncState(
() => assetService.getAssetsForNodeType(nodeType),
[],
{
immediate: false,
resetOnExecute: false,
onError: (err) => {
console.error(
`Error fetching model assets for ${nodeType}:`,
err
)
}
key,
useAsyncState(fetcher, [], {
immediate: false,
resetOnExecute: false,
onError: (err) => {
console.error(`Error fetching model assets for ${key}:`, err)
}
)
})
)
}

const state = stateByNodeType.get(nodeType)!
const state = stateByNodeType.get(key)!

modelLoadingByNodeType.set(nodeType, true)
modelErrorByNodeType.set(nodeType, null)
modelLoadingByNodeType.set(key, true)
modelErrorByNodeType.set(key, null)

try {
await state.execute()
const assets = state.state.value
modelAssetsByNodeType.set(nodeType, assets)
modelErrorByNodeType.set(
nodeType,
state.error.value instanceof Error ? state.error.value : null
)
return assets
} finally {
modelLoadingByNodeType.set(nodeType, state.isLoading.value)
modelLoadingByNodeType.set(key, state.isLoading.value)
}

const assets = state.state.value
const existingAssets = modelAssetsByNodeType.get(key)

if (!isEqual(existingAssets, assets)) {
modelAssetsByNodeType.set(key, assets)
}

modelErrorByNodeType.set(
key,
state.error.value instanceof Error ? state.error.value : null
)

return assets
}

/**
* Fetch and cache model assets for a specific node type
* @param nodeType The node type to fetch assets for (e.g., 'CheckpointLoaderSimple')
* @returns Promise resolving to the fetched assets
*/
async function updateModelsForNodeType(
nodeType: string
): Promise<AssetItem[]> {
return updateModelsForKey(nodeType, () =>
assetService.getAssetsForNodeType(nodeType)
)
}

/**
* Fetch and cache model assets for a specific tag
* @param tag The tag to fetch assets for (e.g., 'models')
* @returns Promise resolving to the fetched assets
*/
async function updateModelsForTag(tag: string): Promise<AssetItem[]> {
const key = `tag:${tag}`
return updateModelsForKey(key, () => assetService.getAssetsByTag(tag))
}

return {
modelAssetsByNodeType,
modelLoadingByNodeType,
modelErrorByNodeType,
updateModelsForNodeType
updateModelsForNodeType,
updateModelsForTag
}
}

return {
modelAssetsByNodeType: shallowReactive(new Map<string, AssetItem[]>()),
modelLoadingByNodeType: shallowReactive(new Map<string, boolean>()),
modelErrorByNodeType: shallowReactive(new Map<string, Error | null>()),
updateModelsForNodeType: async () => []
updateModelsForNodeType: async () => [],
updateModelsForTag: async () => []
}
}

const {
modelAssetsByNodeType,
modelLoadingByNodeType,
modelErrorByNodeType,
updateModelsForNodeType
updateModelsForNodeType,
updateModelsForTag
} = getModelState()

// Watch for completed downloads and refresh model caches
Expand Down Expand Up @@ -403,6 +428,7 @@ export const useAssetsStore = defineStore('assets', () => {
modelAssetsByNodeType,
modelLoadingByNodeType,
modelErrorByNodeType,
updateModelsForNodeType
updateModelsForNodeType,
updateModelsForTag
}
})
Loading
Loading