Add full screen ImageCarousel.tsx
Signed-off-by: Felipe Cardoso <felipe.cardoso@hotmail.it>
This commit is contained in:
@@ -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>
|
||||
|
||||
91
frontend/src/components/ImageCarousel.tsx
Normal file
91
frontend/src/components/ImageCarousel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}`}
|
||||
|
||||
37
frontend/src/components/ui/Modal.tsx
Normal file
37
frontend/src/components/ui/Modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user