Added virtulization for sample images to handle huge number of samples more efficientyly.
This commit is contained in:
@@ -115,7 +115,11 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={`flex flex-col ${className}`}>
|
<div className={`flex flex-col ${className}`}>
|
||||||
<div className="relative w-full" style={{ paddingBottom: '100%' }}>
|
<div className="relative w-full" style={{ paddingBottom: '100%' }}>
|
||||||
<div className="absolute inset-0 rounded-t-lg shadow-md">
|
<div
|
||||||
|
className={classNames('absolute inset-0 rounded-t-lg shadow-md bg-gray-900', {
|
||||||
|
'animate-pulse': isItImage && !loaded,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{isItAVideo && (
|
{isItAVideo && (
|
||||||
<video
|
<video
|
||||||
src={`/api/img/${encodeURIComponent(imageUrl)}`}
|
src={`/api/img/${encodeURIComponent(imageUrl)}`}
|
||||||
|
|||||||
@@ -58,10 +58,16 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
|
|||||||
|
|
||||||
const handleLoad = () => setLoaded(true);
|
const handleLoad = () => setLoaded(true);
|
||||||
|
|
||||||
|
const isImageType = !isAudio(imageUrl) && !isVideo(imageUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col ${className}`}>
|
<div className={`flex flex-col ${className}`}>
|
||||||
<div ref={cardRef} className="relative w-full cursor-pointer" style={{ paddingBottom: '100%' }} onClick={onClick}>
|
<div ref={cardRef} className="relative w-full cursor-pointer" style={{ paddingBottom: '100%' }} onClick={onClick}>
|
||||||
<div className="absolute inset-0 rounded-t-lg shadow-md">
|
<div
|
||||||
|
className={`absolute inset-0 rounded-t-lg shadow-md bg-gray-900 ${
|
||||||
|
isVisible && isImageType && !loaded ? 'animate-pulse' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{isVisible ? (
|
{isVisible ? (
|
||||||
isAudio(imageUrl) ? (
|
isAudio(imageUrl) ? (
|
||||||
<div className="w-full h-full flex items-center justify-center bg-gray-900">
|
<div className="w-full h-full flex items-center justify-center bg-gray-900">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMemo, useState, useRef, useEffect } from 'react';
|
import { useMemo, useState, useRef, useCallback } from 'react';
|
||||||
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
import useSampleImages from '@/hooks/useSampleImages';
|
import useSampleImages from '@/hooks/useSampleImages';
|
||||||
import SampleImageCard from './SampleImageCard';
|
import SampleImageCard from './SampleImageCard';
|
||||||
import { Job } from '@prisma/client';
|
import { Job } from '@prisma/client';
|
||||||
@@ -72,8 +73,9 @@ interface SampleImagesProps {
|
|||||||
export default function SampleImages({ job }: SampleImagesProps) {
|
export default function SampleImages({ job }: SampleImagesProps) {
|
||||||
const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000);
|
const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000);
|
||||||
const [selectedSamplePath, setSelectedSamplePath] = useState<string | null>(null);
|
const [selectedSamplePath, setSelectedSamplePath] = useState<string | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
|
||||||
const didFirstScroll = useRef(false);
|
const scrollParentCallback = useCallback((el: HTMLDivElement | null) => setScrollParent(el), []);
|
||||||
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
const numSamples = useMemo(() => {
|
const numSamples = useMemo(() => {
|
||||||
if (job?.job_config) {
|
if (job?.job_config) {
|
||||||
const jobConfig = JSON.parse(job.job_config) as JobConfig;
|
const jobConfig = JSON.parse(job.job_config) as JobConfig;
|
||||||
@@ -85,16 +87,21 @@ export default function SampleImages({ job }: SampleImagesProps) {
|
|||||||
return 10;
|
return 10;
|
||||||
}, [job]);
|
}, [job]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
// Group samples into rows of `numSamples` for the virtualized list — one row per sample iteration.
|
||||||
if (containerRef.current) {
|
const rows = useMemo(() => {
|
||||||
containerRef.current.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'instant' });
|
const out: string[][] = [];
|
||||||
|
for (let i = 0; i < sampleImages.length; i += numSamples) {
|
||||||
|
out.push(sampleImages.slice(i, i + numSamples));
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
|
}, [sampleImages, numSamples]);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
virtuosoRef.current?.scrollToIndex({ index: 'LAST', align: 'end' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
if (containerRef.current) {
|
virtuosoRef.current?.scrollToIndex({ index: 0, align: 'start' });
|
||||||
containerRef.current.scrollTo({ top: 0, behavior: 'instant' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageInfoContent = useMemo(() => {
|
const PageInfoContent = useMemo(() => {
|
||||||
@@ -247,55 +254,49 @@ export default function SampleImages({ job }: SampleImagesProps) {
|
|||||||
return null;
|
return null;
|
||||||
}, [job]);
|
}, [job]);
|
||||||
|
|
||||||
// scroll to bottom on first load of samples
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === 'success' && sampleImages.length > 0 && !didFirstScroll.current) {
|
|
||||||
didFirstScroll.current = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [status, sampleImages.length]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="absolute top-[80px] left-0 right-0 bottom-0 overflow-y-auto">
|
<div ref={scrollParentCallback} className="absolute top-[80px] left-0 right-0 bottom-0 overflow-y-auto">
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
{PageInfoContent}
|
{PageInfoContent}
|
||||||
{sampleImages && (
|
{sampleImages && rows.length > 0 && scrollParent && (
|
||||||
<div className={`grid ${gridColsClass} gap-1`}>
|
<Virtuoso
|
||||||
{sampleImages.map((sample: string, idx: number) => {
|
ref={virtuosoRef}
|
||||||
// Compute current group (groups are size = numSamples)
|
customScrollParent={scrollParent}
|
||||||
const groupIndex = Math.floor(idx / numSamples);
|
totalCount={rows.length}
|
||||||
const groupStart = groupIndex * numSamples;
|
initialTopMostItemIndex={rows.length - 1}
|
||||||
const groupEnd = Math.min(groupStart + numSamples, sampleImages.length);
|
followOutput="auto"
|
||||||
const groupSize = groupEnd - groupStart;
|
increaseViewportBy={400}
|
||||||
const isEndOfGroup = idx === groupEnd - 1;
|
computeItemKey={index => rows[index]?.[0] ?? index}
|
||||||
|
itemContent={index => {
|
||||||
|
const row = rows[index];
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
// Only enforce a MIN of 3 when the group's planned width is < 3
|
// Only pad the final row when numSamples < MIN_COLS and the row is short.
|
||||||
const MIN_COLS = 3;
|
const MIN_COLS = 3;
|
||||||
const shouldPad = numSamples < MIN_COLS && groupSize < MIN_COLS;
|
const shouldPad = numSamples < MIN_COLS && row.length < MIN_COLS;
|
||||||
const padsNeeded = shouldPad ? MIN_COLS - groupSize : 0;
|
const padsNeeded = shouldPad ? MIN_COLS - row.length : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={sample} className="contents">
|
// pb-1 recreates the vertical gap between rows that the original single CSS grid provided via `gap-1`.
|
||||||
<SampleImageCard
|
<div className={`grid ${gridColsClass} gap-1 pb-1`}>
|
||||||
imageUrl={sample}
|
{row.map(sample => (
|
||||||
numSamples={numSamples}
|
<SampleImageCard
|
||||||
sampleImages={sampleImages}
|
key={sample}
|
||||||
alt="Sample Image"
|
imageUrl={sample}
|
||||||
onClick={() => setSelectedSamplePath(sample)}
|
numSamples={numSamples}
|
||||||
observerRoot={containerRef.current}
|
sampleImages={sampleImages}
|
||||||
/>
|
alt="Sample Image"
|
||||||
|
onClick={() => setSelectedSamplePath(sample)}
|
||||||
{isEndOfGroup &&
|
observerRoot={scrollParent}
|
||||||
padsNeeded > 0 &&
|
/>
|
||||||
Array.from({ length: padsNeeded }).map((_, i) => (
|
))}
|
||||||
<div key={`pad-${groupIndex}-${i}`} className="invisible" />
|
{Array.from({ length: padsNeeded }).map((_, i) => (
|
||||||
))}
|
<div key={`pad-${index}-${i}`} className="invisible" />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}}
|
||||||
</div>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SampleImageViewer
|
<SampleImageViewer
|
||||||
|
|||||||
Reference in New Issue
Block a user