Skip to content

Commit 4b69170

Browse files
viva-jinyiclaude
authored andcommitted
feat: Add pagination support for media assets history (#6373)
## Summary - Implement pagination for media assets history to handle large datasets efficiently - Add infinite scroll support with approach-end event handler - Support offset parameter in history API for both V1 and V2 endpoints ## Changes - Add offset parameter support to `api.getHistory()` method - Update history fetchers (V1/V2) to include offset in API requests - Implement `loadMoreHistory()` in assetsStore with pagination state management - Add `loadMore`, `hasMore`, and `isLoadingMore` to IAssetsProvider interface - Add approach-end handler in AssetsSidebarTab for infinite scroll - Set BATCH_SIZE to 200 for efficient loading ## Implementation Improvements Simplified offset-based pagination by removing unnecessary reconciliation logic: - Remove `reconcileHistory`, `taskItemsMap`, `lastKnownQueueIndex` (offset is sufficient) - Replace `assetItemsByPromptId` Map → `loadedIds` Set (store IDs only) - Replace `findInsertionIndex` binary search → push + sort (faster for batch operations) - Replace `loadingPromise` → `isLoadingMore` boolean (simpler state management) - Fix memory leak by cleaning up Set together with array slice ## Test Plan - [x] TypeScript compilation passes - [x] ESLint and Prettier formatting applied - [x] Test infinite scroll in media assets tab - [x] Verify network requests include correct offset parameter - [x] Confirm no duplicate items when loading more 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8b257c9 commit 4b69170

File tree

15 files changed

+733
-220
lines changed

15 files changed

+733
-220
lines changed

src/components/sidebar/tabs/AssetSidebarTemplate.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
<slot name="header" />
1515
</div>
1616
</div>
17-
<!-- h-0 to force scrollpanel to grow -->
18-
<ScrollPanel class="h-0 grow">
17+
<!-- min-h-0 to force scrollpanel to grow -->
18+
<ScrollPanel class="min-h-0 grow">
1919
<slot name="body" />
2020
</ScrollPanel>
2121
<div v-if="slots.footer">

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,35 @@
4141
</TabList>
4242
</template>
4343
<template #body>
44-
<div v-if="displayAssets.length" class="relative size-full">
44+
<!-- Loading state -->
45+
<div v-if="loading">
46+
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
47+
</div>
48+
<!-- Empty state -->
49+
<div v-else-if="!displayAssets.length">
50+
<NoResultsPlaceholder
51+
icon="pi pi-info-circle"
52+
:title="
53+
$t(
54+
activeTab === 'input'
55+
? 'sideToolbar.noImportedFiles'
56+
: 'sideToolbar.noGeneratedFiles'
57+
)
58+
"
59+
:message="$t('sideToolbar.noFilesFoundMessage')"
60+
/>
61+
</div>
62+
<!-- Content -->
63+
<div v-else class="relative size-full">
4564
<VirtualGrid
46-
v-if="displayAssets.length"
4765
:items="mediaAssetsWithKey"
4866
:grid-style="{
4967
display: 'grid',
5068
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
5169
padding: '0.5rem',
5270
gap: '0.5rem'
5371
}"
72+
@approach-end="handleApproachEnd"
5473
>
5574
<template #item="{ item }">
5675
<MediaAssetCard
@@ -66,24 +85,6 @@
6685
/>
6786
</template>
6887
</VirtualGrid>
69-
<div v-else-if="loading">
70-
<ProgressSpinner
71-
class="absolute left-1/2 w-[50px] -translate-x-1/2"
72-
/>
73-
</div>
74-
<div v-else>
75-
<NoResultsPlaceholder
76-
icon="pi pi-info-circle"
77-
:title="
78-
$t(
79-
activeTab === 'input'
80-
? 'sideToolbar.noImportedFiles'
81-
: 'sideToolbar.noGeneratedFiles'
82-
)
83-
"
84-
:message="$t('sideToolbar.noFilesFoundMessage')"
85-
/>
86-
</div>
8788
</div>
8889
</template>
8990
<template #footer>
@@ -147,6 +148,7 @@
147148
</template>
148149

149150
<script setup lang="ts">
151+
import { useDebounceFn } from '@vueuse/core'
150152
import ProgressSpinner from 'primevue/progressspinner'
151153
import { useToast } from 'primevue/usetoast'
152154
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -291,6 +293,7 @@ watch(
291293
activeTab,
292294
() => {
293295
clearSelection()
296+
// Reset pagination state when tab changes
294297
void refreshAssets()
295298
},
296299
{ immediate: true }
@@ -395,4 +398,15 @@ const handleDeleteSelected = async () => {
395398
await deleteMultipleAssets(selectedAssets)
396399
clearSelection()
397400
}
401+
402+
const handleApproachEnd = useDebounceFn(async () => {
403+
if (
404+
activeTab.value === 'output' &&
405+
!isInFolderView.value &&
406+
outputAssets.hasMore.value &&
407+
!outputAssets.isLoadingMore.value
408+
) {
409+
await outputAssets.loadMore()
410+
}
411+
}, 300)
398412
</script>

src/platform/assets/components/Media3DBottom.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<template>
22
<div class="flex flex-col items-center gap-1">
33
<MediaTitle :file-name="fileName" />
4-
<div class="flex items-center gap-2 text-xs text-zinc-400">
4+
<!-- TBD: File size will be provided by backend history API -->
5+
<div
6+
v-if="asset.size"
7+
class="flex items-center gap-2 text-xs text-zinc-400"
8+
>
59
<span>{{ formatSize(asset.size) }}</span>
610
</div>
711
</div>

src/platform/assets/components/MediaAudioBottom.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<template>
22
<div class="flex flex-col items-center gap-1">
33
<MediaTitle :file-name="fileName" />
4-
<div class="flex items-center gap-2 text-xs text-zinc-400">
4+
<!-- TBD: File size will be provided by backend history API -->
5+
<div
6+
v-if="asset.size"
7+
class="flex items-center gap-2 text-xs text-zinc-400"
8+
>
59
<span>{{ formatSize(asset.size) }}</span>
610
</div>
711
</div>

src/platform/assets/components/MediaVideoBottom.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<template>
22
<div class="flex flex-col items-center gap-1">
33
<MediaTitle :file-name="fileName" />
4-
<div class="flex items-center text-xs text-zinc-400">
4+
<!-- TBD: File size will be provided by backend history API -->
5+
<div v-if="asset.size" class="flex items-center text-xs text-zinc-400">
56
<span>{{ formatSize(asset.size) }}</span>
67
</div>
78
</div>

src/platform/assets/components/MediaVideoTop.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
<video
88
ref="videoRef"
99
:controls="shouldShowControls"
10-
preload="none"
10+
preload="metadata"
11+
autoplay
12+
muted
13+
loop
14+
playsinline
1115
:poster="asset.preview_url"
1216
class="relative size-full object-contain"
1317
@click.stop

src/platform/assets/composables/media/IAssetsProvider.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,19 @@ export interface IAssetsProvider {
2626
* Refresh the media list (alias for fetchMediaList)
2727
*/
2828
refresh: () => Promise<AssetItem[]>
29+
30+
/**
31+
* Load more items (for pagination)
32+
*/
33+
loadMore: () => Promise<void>
34+
35+
/**
36+
* Whether there are more items to load
37+
*/
38+
hasMore: Ref<boolean>
39+
40+
/**
41+
* Whether currently loading more items
42+
*/
43+
isLoadingMore: Ref<boolean>
2944
}

src/platform/assets/composables/media/useAssetsApi.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,28 @@ export function useAssetsApi(directory: 'input' | 'output') {
3636

3737
const refresh = () => fetchMediaList()
3838

39+
const loadMore = async (): Promise<void> => {
40+
if (directory === 'output') {
41+
await assetsStore.loadMoreHistory()
42+
}
43+
}
44+
45+
const hasMore = computed(() => {
46+
return directory === 'output' ? assetsStore.hasMoreHistory : false
47+
})
48+
49+
const isLoadingMore = computed(() => {
50+
return directory === 'output' ? assetsStore.isLoadingMore : false
51+
})
52+
3953
return {
4054
media,
4155
loading,
4256
error,
4357
fetchMediaList,
44-
refresh
58+
refresh,
59+
loadMore,
60+
hasMore,
61+
isLoadingMore
4562
}
4663
}

src/platform/assets/composables/media/useInternalFilesApi.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,28 @@ export function useInternalFilesApi(directory: 'input' | 'output') {
3636

3737
const refresh = () => fetchMediaList()
3838

39+
const loadMore = async (): Promise<void> => {
40+
if (directory === 'output') {
41+
await assetsStore.loadMoreHistory()
42+
}
43+
}
44+
45+
const hasMore = computed(() => {
46+
return directory === 'output' ? assetsStore.hasMoreHistory : false
47+
})
48+
49+
const isLoadingMore = computed(() => {
50+
return directory === 'output' ? assetsStore.isLoadingMore : false
51+
})
52+
3953
return {
4054
media,
4155
loading,
4256
error,
4357
fetchMediaList,
44-
refresh
58+
refresh,
59+
loadMore,
60+
hasMore,
61+
isLoadingMore
4562
}
4663
}

src/platform/assets/schemas/assetSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const zAsset = z.object({
55
id: z.string(),
66
name: z.string(),
77
asset_hash: z.string().nullish(),
8-
size: z.number(),
8+
size: z.number().optional(), // TBD: Will be provided by history API in the future
99
mime_type: z.string().nullish(),
1010
tags: z.array(z.string()).optional().default([]),
1111
preview_id: z.string().nullable().optional(),

0 commit comments

Comments
 (0)