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 {useSamples} from '@/contexts/SamplesContext'
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import {useMemo} from 'react'
|
import {useMemo, useState} from 'react'
|
||||||
import {useTraining} from "@/contexts/TrainingContext";
|
import {useTraining} from "@/contexts/TrainingContext";
|
||||||
import {SampleCard} from "@/components/SampleCard";
|
import {SampleCard} from "@/components/SampleCard";
|
||||||
|
import {ImageCarousel} from "@/components/ImageCarousel";
|
||||||
|
import {Modal} from "@/components/ui/Modal";
|
||||||
|
|
||||||
// Helper function to parse sample information
|
// Helper function to parse sample information
|
||||||
const parseSampleInfo = (filename: string) => {
|
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() {
|
export default function SamplesPage() {
|
||||||
const {samples, isLoading: samplesLoading, error: samplesError} = useSamples()
|
const {samples, isLoading: samplesLoading, error: samplesError} = useSamples()
|
||||||
const {config, isLoading: configLoading} = useTraining()
|
const {config, isLoading: configLoading} = useTraining()
|
||||||
|
const [selectedImage, setSelectedImage] = useState<SelectedImage>(null);
|
||||||
|
|
||||||
|
|
||||||
// Get prompts from config
|
// Get prompts from config
|
||||||
const prompts = config?.config?.process[0]?.sample?.prompts || []
|
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]))
|
return new Map([...groups].sort((a, b) => b[0] - a[0]))
|
||||||
}, [samples])
|
}, [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
|
// Handle loading and error states
|
||||||
if (samplesLoading || configLoading) return (
|
if (samplesLoading || configLoading) return (
|
||||||
<div className="min-h-screen bg-gray-900 p-4">
|
<div className="min-h-screen bg-gray-900 p-4">
|
||||||
@@ -99,26 +117,47 @@ export default function SamplesPage() {
|
|||||||
|
|
||||||
{/* Sample groups */}
|
{/* Sample groups */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{Array.from(groupedSamples).map(([batch, items]) => (
|
{Array.from(groupedSamples).map(([batch, items], batchIndex) => {
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Scrollable container for samples */}
|
const batchImages = getImagesForBatch(items, batch);
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<div className="flex gap-4 min-w-full pb-4">
|
return (
|
||||||
{items.map((sample: any) => (
|
<div key={batch} className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6">
|
||||||
<SampleCard
|
<h2 className="text-xl font-semibold text-gray-300 mb-4">
|
||||||
key={sample.filename}
|
Steps {batch}
|
||||||
sample={sample}
|
</h2>
|
||||||
prompt={prompts[sample.index]}
|
|
||||||
/>
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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 {
|
interface SampleCardProps {
|
||||||
sample: {
|
sample: {
|
||||||
filename: string
|
filename: string;
|
||||||
url: string
|
url: string;
|
||||||
created_at: string
|
created_at: string;
|
||||||
index: number
|
index: number;
|
||||||
}
|
};
|
||||||
prompt?: string // Associated prompt from config
|
prompt?: string;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SampleCard({sample, prompt}: SampleCardProps) {
|
export function SampleCard({sample, prompt, onClick}: SampleCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 group">
|
<div className="flex-shrink-0 group">
|
||||||
<div
|
<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
|
<Image
|
||||||
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
|
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
|
||||||
alt={`Sample ${sample.index}`}
|
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