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>
This commit is contained in:
Jin Yi
2025-11-14 04:15:44 +09:00
committed by GitHub
parent e639577685
commit f0f554392d
15 changed files with 735 additions and 222 deletions

View File

@@ -1,7 +1,11 @@
<template>
<div class="flex flex-col items-center gap-1">
<MediaTitle :file-name="fileName" />
<div class="flex items-center gap-2 text-xs text-zinc-400">
<!-- TBD: File size will be provided by backend history API -->
<div
v-if="asset.size"
class="flex items-center gap-2 text-xs text-zinc-400"
>
<span>{{ formatSize(asset.size) }}</span>
</div>
</div>

View File

@@ -1,7 +1,11 @@
<template>
<div class="flex flex-col items-center gap-1">
<MediaTitle :file-name="fileName" />
<div class="flex items-center gap-2 text-xs text-zinc-400">
<!-- TBD: File size will be provided by backend history API -->
<div
v-if="asset.size"
class="flex items-center gap-2 text-xs text-zinc-400"
>
<span>{{ formatSize(asset.size) }}</span>
</div>
</div>

View File

@@ -1,7 +1,8 @@
<template>
<div class="flex flex-col items-center gap-1">
<MediaTitle :file-name="fileName" />
<div class="flex items-center text-xs text-zinc-400">
<!-- TBD: File size will be provided by backend history API -->
<div v-if="asset.size" class="flex items-center text-xs text-zinc-400">
<span>{{ formatSize(asset.size) }}</span>
</div>
</div>

View File

@@ -7,7 +7,11 @@
<video
ref="videoRef"
:controls="shouldShowControls"
preload="none"
preload="metadata"
autoplay
muted
loop
playsinline
:poster="asset.preview_url"
class="relative size-full object-contain"
@click.stop

View File

@@ -26,4 +26,19 @@ export interface IAssetsProvider {
* Refresh the media list (alias for fetchMediaList)
*/
refresh: () => Promise<AssetItem[]>
/**
* Load more items (for pagination)
*/
loadMore: () => Promise<void>
/**
* Whether there are more items to load
*/
hasMore: Ref<boolean>
/**
* Whether currently loading more items
*/
isLoadingMore: Ref<boolean>
}

View File

@@ -36,11 +36,28 @@ export function useAssetsApi(directory: 'input' | 'output') {
const refresh = () => fetchMediaList()
const loadMore = async (): Promise<void> => {
if (directory === 'output') {
await assetsStore.loadMoreHistory()
}
}
const hasMore = computed(() => {
return directory === 'output' ? assetsStore.hasMoreHistory : false
})
const isLoadingMore = computed(() => {
return directory === 'output' ? assetsStore.isLoadingMore : false
})
return {
media,
loading,
error,
fetchMediaList,
refresh
refresh,
loadMore,
hasMore,
isLoadingMore
}
}

View File

@@ -36,11 +36,28 @@ export function useInternalFilesApi(directory: 'input' | 'output') {
const refresh = () => fetchMediaList()
const loadMore = async (): Promise<void> => {
if (directory === 'output') {
await assetsStore.loadMoreHistory()
}
}
const hasMore = computed(() => {
return directory === 'output' ? assetsStore.hasMoreHistory : false
})
const isLoadingMore = computed(() => {
return directory === 'output' ? assetsStore.isLoadingMore : false
})
return {
media,
loading,
error,
fetchMediaList,
refresh
refresh,
loadMore,
hasMore,
isLoadingMore
}
}

View File

@@ -5,7 +5,7 @@ const zAsset = z.object({
id: z.string(),
name: z.string(),
asset_hash: z.string().nullish(),
size: z.number(),
size: z.number().optional(), // TBD: Will be provided by history API in the future
mime_type: z.string().nullish(),
tags: z.array(z.string()).optional().default([]),
preview_id: z.string().nullable().optional(),

View File

@@ -15,13 +15,28 @@ import type {
* Fetches history from V1 API endpoint
* @param api - API instance with fetchApi method
* @param maxItems - Maximum number of history items to fetch
* @param offset - Offset for pagination (must be non-negative integer)
* @returns Promise resolving to V1 history response
* @throws Error if offset is invalid (negative or non-integer)
*/
export async function fetchHistoryV1(
fetchApi: (url: string) => Promise<Response>,
maxItems: number = 200
maxItems: number = 200,
offset?: number
): Promise<HistoryV1Response> {
const res = await fetchApi(`/history?max_items=${maxItems}`)
// Validate offset parameter
if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
throw new Error(
`Invalid offset parameter: ${offset}. Must be a non-negative integer.`
)
}
const params = new URLSearchParams({ max_items: maxItems.toString() })
if (offset !== undefined) {
params.set('offset', offset.toString())
}
const url = `/history?${params.toString()}`
const res = await fetchApi(url)
const json: Record<
string,
Omit<HistoryTaskItem, 'taskType'>

View File

@@ -14,13 +14,28 @@ import type { HistoryResponseV2 } from '../types/historyV2Types'
* Fetches history from V2 API endpoint and adapts to V1 format
* @param fetchApi - API instance with fetchApi method
* @param maxItems - Maximum number of history items to fetch
* @param offset - Offset for pagination (must be non-negative integer)
* @returns Promise resolving to V1 history response (adapted from V2)
* @throws Error if offset is invalid (negative or non-integer)
*/
export async function fetchHistoryV2(
fetchApi: (url: string) => Promise<Response>,
maxItems: number = 200
maxItems: number = 200,
offset?: number
): Promise<HistoryV1Response> {
const res = await fetchApi(`/history_v2?max_items=${maxItems}`)
// Validate offset parameter
if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
throw new Error(
`Invalid offset parameter: ${offset}. Must be a non-negative integer.`
)
}
const params = new URLSearchParams({ max_items: maxItems.toString() })
if (offset !== undefined) {
params.set('offset', offset.toString())
}
const url = `/history_v2?${params.toString()}`
const res = await fetchApi(url)
const rawData: HistoryResponseV2 = await res.json()
const adaptedHistory = mapHistoryV2toHistory(rawData)
return { History: adaptedHistory }