From 3a7906516340c997b639e45eba7d85443ee982a8 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Thu, 23 Jan 2025 15:08:39 +0100 Subject: [PATCH] Add full screen ImageCarousel.tsx Signed-off-by: Felipe Cardoso --- frontend/src/app/samples/page.tsx | 75 ++++++++++++++----- frontend/src/components/ImageCarousel.tsx | 91 +++++++++++++++++++++++ frontend/src/components/SampleCard.tsx | 20 +++-- frontend/src/components/ui/Modal.tsx | 37 +++++++++ 4 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/ImageCarousel.tsx create mode 100644 frontend/src/components/ui/Modal.tsx diff --git a/frontend/src/app/samples/page.tsx b/frontend/src/app/samples/page.tsx index db34f24..570ef33 100644 --- a/frontend/src/app/samples/page.tsx +++ b/frontend/src/app/samples/page.tsx @@ -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(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 (
@@ -99,26 +117,47 @@ export default function SamplesPage() { {/* Sample groups */}
- {Array.from(groupedSamples).map(([batch, items]) => ( -
-

- Steps {batch} -

+ {Array.from(groupedSamples).map(([batch, items], batchIndex) => { - {/* Scrollable container for samples */} -
-
- {items.map((sample: any) => ( - - ))} + const batchImages = getImagesForBatch(items, batch); + + return ( +
+

+ Steps {batch} +

+ +
+
+ {items.map((sample: any, imageIndex: number) => ( + setSelectedImage({ + batchIndex, + imageIndex + })} + /> + ))} +
+ + {selectedImage && selectedImage.batchIndex === batchIndex && ( + setSelectedImage(null)} + > + setSelectedImage(null)} + /> + + )}
-
- ))} + ); + })}
diff --git a/frontend/src/components/ImageCarousel.tsx b/frontend/src/components/ImageCarousel.tsx new file mode 100644 index 0000000..15ae49d --- /dev/null +++ b/frontend/src/components/ImageCarousel.tsx @@ -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 ( +
+ {/* Navigation bar */} +
+ +
+ {currentIndex + 1} / {images.length} +
+
+ + {/* Main content area */} +
+ {/* Previous button */} + + + {/* Image and prompt */} +
+
+ {`Sample + {currentImage.prompt && ( +
+ {currentImage.prompt} +
+ )} +
+
+ + {/* Next button */} + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/SampleCard.tsx b/frontend/src/components/SampleCard.tsx index 258e9c0..ba35f54 100644 --- a/frontend/src/components/SampleCard.tsx +++ b/frontend/src/components/SampleCard.tsx @@ -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 (
+ className="w-48 overflow-hidden rounded-lg bg-gray-700 shadow-md transition-all duration-200 group-hover/card:shadow-lg" + onClick={onClick} + > + {`Sample 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 ( +
+ {/* Stop click propagation to prevent closing when clicking content */} +
e.stopPropagation()} + > + {children} +
+
+ ); +} \ No newline at end of file