Add full screen ImageCarousel.tsx

Signed-off-by: Felipe Cardoso <felipe.cardoso@hotmail.it>
This commit is contained in:
2025-01-23 15:08:39 +01:00
parent 5c55d9f8ba
commit 3a79065163
4 changed files with 197 additions and 26 deletions

View File

@@ -3,9 +3,11 @@
import {useSamples} from '@/contexts/SamplesContext'
import Link from "next/link"
import {useMemo} from 'react'
import {useMemo, useState} from 'react'
import {useTraining} from "@/contexts/TrainingContext";
import {SampleCard} from "@/components/SampleCard";
import {ImageCarousel} from "@/components/ImageCarousel";
import {Modal} from "@/components/ui/Modal";
// Helper function to parse sample information
const parseSampleInfo = (filename: string) => {
@@ -18,9 +20,16 @@ const parseSampleInfo = (filename: string) => {
}
}
type SelectedImage = {
batchIndex: number; // To track which batch we're in
imageIndex: number; // To track which image in the batch
} | null;
export default function SamplesPage() {
const {samples, isLoading: samplesLoading, error: samplesError} = useSamples()
const {config, isLoading: configLoading} = useTraining()
const [selectedImage, setSelectedImage] = useState<SelectedImage>(null);
// Get prompts from config
const prompts = config?.config?.process[0]?.sample?.prompts || []
@@ -50,6 +59,15 @@ export default function SamplesPage() {
return new Map([...groups].sort((a, b) => b[0] - a[0]))
}, [samples])
const getImagesForBatch = (batchItems: any[], batch: any) => {
return batchItems.map(sample => ({
url: sample.url,
index: sample.index,
prompt: prompts[sample.index],
batch: batch
}));
};
// Handle loading and error states
if (samplesLoading || configLoading) return (
<div className="min-h-screen bg-gray-900 p-4">
@@ -99,26 +117,47 @@ export default function SamplesPage() {
{/* Sample groups */}
<div className="space-y-8">
{Array.from(groupedSamples).map(([batch, items]) => (
<div key={batch} className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6">
<h2 className="text-xl font-semibold text-gray-300 mb-4">
Steps {batch}
</h2>
{Array.from(groupedSamples).map(([batch, items], batchIndex) => {
{/* Scrollable container for samples */}
<div className="overflow-x-auto">
<div className="flex gap-4 min-w-full pb-4">
{items.map((sample: any) => (
<SampleCard
key={sample.filename}
sample={sample}
prompt={prompts[sample.index]}
/>
))}
const batchImages = getImagesForBatch(items, batch);
return (
<div key={batch} className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6">
<h2 className="text-xl font-semibold text-gray-300 mb-4">
Steps {batch}
</h2>
<div className="overflow-x-auto">
<div className="flex gap-4 min-w-full pb-4">
{items.map((sample: any, imageIndex: number) => (
<SampleCard
key={sample.filename}
sample={sample}
prompt={prompts[sample.index]}
onClick={() => setSelectedImage({
batchIndex,
imageIndex
})}
/>
))}
</div>
</div>
{selectedImage && selectedImage.batchIndex === batchIndex && (
<Modal
isOpen={true}
onClose={() => setSelectedImage(null)}
>
<ImageCarousel
images={batchImages}
initialIndex={selectedImage.imageIndex}
onClose={() => setSelectedImage(null)}
/>
</Modal>
)}
</div>
</div>
))}
);
})}
</div>
</div>
</main>

View File

@@ -0,0 +1,91 @@
"use client"
import Image from 'next/image';
import {useEffect, useState} from 'react';
interface CarouselImage {
url: string;
index: number;
prompt?: string;
batch?: number;
}
interface ImageCarouselProps {
images: CarouselImage[];
initialIndex: number;
onClose: () => void;
}
export function ImageCarousel({images, initialIndex, onClose}: ImageCarouselProps) {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
// Navigate between images
const goToNext = () => setCurrentIndex((prev) => (prev + 1) % images.length);
const goToPrevious = () => setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
// Handle keyboard navigation
useEffect(() => {
const handleKeyboard = (e: KeyboardEvent) => {
if (e.key === 'ArrowRight') goToNext();
if (e.key === 'ArrowLeft') goToPrevious();
};
window.addEventListener('keydown', handleKeyboard);
return () => window.removeEventListener('keydown', handleKeyboard);
}, []);
const currentImage = images[currentIndex];
return (
<div className="fixed inset-0 flex flex-col">
{/* Navigation bar */}
<div className="flex items-center justify-between p-4 bg-gray-800/80 text-gray-200">
<button
onClick={onClose}
className="hover:text-white transition-colors"
>
Close
</button>
<div className="text-sm">
{currentIndex + 1} / {images.length}
</div>
</div>
{/* Main content area */}
<div className="flex-1 flex items-center justify-center relative">
{/* Previous button */}
<button
onClick={goToPrevious}
className="absolute left-4 p-4 text-gray-400 hover:text-white transition-colors"
>
</button>
{/* Image and prompt */}
<div className="max-w-4xl max-h-full p-4">
<div className="relative">
<Image
src={`${process.env.NEXT_PUBLIC_API_URL}${currentImage.url}`}
alt={`Sample ${currentImage.index}`}
width={1024}
height={1024}
className="max-h-[80vh] w-auto object-contain"
/>
{currentImage.prompt && (
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white p-4 text-sm">
{currentImage.prompt}
</div>
)}
</div>
</div>
{/* Next button */}
<button
onClick={goToNext}
className="absolute right-4 p-4 text-gray-400 hover:text-white transition-colors"
>
</button>
</div>
</div>
);
}

View File

@@ -4,19 +4,23 @@ import Image from 'next/image'
interface SampleCardProps {
sample: {
filename: string
url: string
created_at: string
index: number
}
prompt?: string // Associated prompt from config
filename: string;
url: string;
created_at: string;
index: number;
};
prompt?: string;
onClick?: () => void;
}
export function SampleCard({sample, prompt}: SampleCardProps) {
export function SampleCard({sample, prompt, onClick}: SampleCardProps) {
return (
<div className="flex-shrink-0 group">
<div
className="w-48 overflow-hidden rounded-lg bg-gray-700 shadow-md transition-all duration-200 group-hover/card:shadow-lg">
className="w-48 overflow-hidden rounded-lg bg-gray-700 shadow-md transition-all duration-200 group-hover/card:shadow-lg"
onClick={onClick}
>
<Image
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
alt={`Sample ${sample.index}`}

View File

@@ -0,0 +1,37 @@
"use client"
import React from "react";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export function Modal({isOpen, onClose, children}: ModalProps) {
// If modal is not open, don't render anything
if (!isOpen) return null;
// Handle escape key press
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [onClose]);
return (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center"
onClick={onClose}
>
{/* Stop click propagation to prevent closing when clicking content */}
<div
className="relative"
onClick={e => e.stopPropagation()}
>
{children}
</div>
</div>
);
}