Skip to content
Open
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
21 changes: 20 additions & 1 deletion ui/src/components/SampleImageCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import React, { useRef, useEffect, useState, ReactNode } from 'react';
import { isVideo, isAudio } from '@/utils/basic';
import { getFilename, isVideo, isAudio } from '@/utils/basic';

function sampleLabel(filename: string) {
const match = filename.match(/^.+__(\d+)_(\d+)\.[^.]+$/);
if (!match) return filename;

return `Step ${Number(match[1]).toLocaleString()} · Sample ${Number(match[2])}`;
}

interface SampleImageCardProps {
imageUrl: string;
Expand Down Expand Up @@ -36,6 +43,8 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
const isItAudio = isAudio(imageUrl);
const isItVideo = isVideo(imageUrl);
const isImageType = !isItAudio && !isItVideo;
const filename = getFilename(imageUrl);
const label = sampleLabel(filename);

// Observe both enter and exit
useEffect(() => {
Expand Down Expand Up @@ -119,6 +128,13 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/20" aria-hidden="true">
<span className="rounded-full border border-gray-700 bg-gray-950/80 p-4 text-gray-200 shadow-md">
<svg width="28" height="28" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8.5 5.5v13l11-6.5-11-6.5z" fill="currentColor" />
</svg>
</span>
</div>
</div>
) : isItVideo ? (
<video
Expand All @@ -140,6 +156,9 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
{children && isVisible && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
</div>
</div>
<div className="mt-1 truncate px-1 text-center text-xs text-gray-500 dark:text-gray-400" title={filename}>
{label}
</div>
</div>
);
};
Expand Down
27 changes: 19 additions & 8 deletions ui/src/components/SampleImages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { apiClient } from '@/utils/api';
import classNames from 'classnames';
import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
import SampleImageViewer from './SampleImageViewer';
import { isAudio } from '@/utils/basic';

interface SampleImagesMenuProps {
job?: Job | null;
Expand Down Expand Up @@ -87,14 +88,17 @@ export default function SampleImages({ job }: SampleImagesProps) {
return 10;
}, [job]);

// Group samples into rows of `numSamples` for the virtualized list — one row per sample iteration.
const isAudioSamples = useMemo(() => sampleImages.length > 0 && sampleImages.every(isAudio), [sampleImages]);
const samplesPerRow = isAudioSamples ? Math.max(numSamples, 6) : numSamples;

// Keep image and video samples grouped by iteration; use a denser grid for audio samples.
const rows = useMemo(() => {
const out: string[][] = [];
for (let i = 0; i < sampleImages.length; i += numSamples) {
out.push(sampleImages.slice(i, i + numSamples));
for (let i = 0; i < sampleImages.length; i += samplesPerRow) {
out.push(sampleImages.slice(i, i + samplesPerRow));
}
return out;
}, [sampleImages, numSamples]);
}, [sampleImages, samplesPerRow]);

const scrollToBottom = () => {
virtuosoRef.current?.scrollToIndex({ index: 'LAST', align: 'end' });
Expand Down Expand Up @@ -158,7 +162,7 @@ export default function SampleImages({ job }: SampleImagesProps) {

// Inline style instead of Tailwind grid-cols-N classes — Tailwind only ships grid-cols-1..12,
// so class-based columns silently break for larger sample counts.
const gridCols = Math.max(numSamples, 3);
const gridCols = Math.max(samplesPerRow, 3);

const sampleConfig = useMemo(() => {
if (job?.job_config) {
Expand All @@ -185,14 +189,21 @@ export default function SampleImages({ job }: SampleImagesProps) {
const row = rows[index];
if (!row) return null;

// Only pad the final row when numSamples < MIN_COLS and the row is short.
// Only pad the final row when samplesPerRow < MIN_COLS and the row is short.
const MIN_COLS = 3;
const shouldPad = numSamples < MIN_COLS && row.length < MIN_COLS;
const shouldPad = samplesPerRow < MIN_COLS && row.length < MIN_COLS;
const padsNeeded = shouldPad ? MIN_COLS - row.length : 0;

return (
// pb-1 recreates the vertical gap between rows that the original single CSS grid provided via `gap-1`.
<div className="grid gap-1 pb-1" style={{ gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))` }}>
<div
className="grid gap-2 pb-3"
style={
isAudioSamples
? { gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 180px))' }
: { gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))` }
}
>
{row.map(sample => (
<SampleImageCard
key={sample}
Expand Down