From 469e07e046c24449213dc57bd4541e7b5e56f17e Mon Sep 17 00:00:00 2001 From: Desktop Date: Thu, 13 Nov 2025 00:07:34 +0500 Subject: [PATCH] lightbox --- .../components/common/ContentImageCell.tsx | 12 ++- frontend/src/config/pages/images.config.tsx | 13 ++- frontend/src/pages/Writer/Images.tsx | 101 +++++++++++++++++- 3 files changed, 121 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/common/ContentImageCell.tsx b/frontend/src/components/common/ContentImageCell.tsx index 85027bcd..dea0ca99 100644 --- a/frontend/src/components/common/ContentImageCell.tsx +++ b/frontend/src/components/common/ContentImageCell.tsx @@ -20,9 +20,10 @@ export interface ContentImageData { interface ContentImageCellProps { image: ContentImageData | null; maxPromptLength?: number; + onImageClick?: () => void; } -export default function ContentImageCell({ image, maxPromptLength = 100 }: ContentImageCellProps) { +export default function ContentImageCell({ image, maxPromptLength = 100, onImageClick }: ContentImageCellProps) { const [showFullPrompt, setShowFullPrompt] = useState(false); // Check if image_path is a valid local file path (not a URL) @@ -113,7 +114,14 @@ export default function ContentImageCell({ image, maxPromptLength = 100 }: Conte {prompt { + if (onImageClick) { + onImageClick(); + } + }} onError={(e) => { // Show empty placeholder if local image fails to load const target = e.target as HTMLImageElement; diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index 6b9663d9..63f23641 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -52,6 +52,7 @@ export const createImagesPageConfig = ( setCurrentPage: (page: number) => void; maxInArticleImages?: number; // Optional: max in-article images to display onGenerateImages?: (contentId: number) => void; // Handler for generate images button + onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void; // Handler for image click } ): ImagesPageConfig => { const maxImages = handlers.maxInArticleImages || 5; // Default to 5 in-article images @@ -84,7 +85,10 @@ export const createImagesPageConfig = ( sortable: false, width: '200px', render: (_value: any, row: ContentImagesGroup) => ( - + handlers.onImageClick!(row.content_id, 'featured') : undefined} + /> ), }, ]; @@ -98,7 +102,12 @@ export const createImagesPageConfig = ( width: '200px', render: (_value: any, row: ContentImagesGroup) => { const image = row.in_article_images.find(img => img.position === i); - return ; + return ( + handlers.onImageClick!(row.content_id, 'in_article', i) : undefined} + /> + ); }, }); } diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 6707f505..d86ec590 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -12,6 +12,7 @@ import { fetchImageGenerationSettings, generateImages, bulkUpdateImagesStatus, + ContentImage, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon } from '../../icons'; @@ -20,6 +21,8 @@ import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQu import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal'; import { useResourceDebug } from '../../hooks/useResourceDebug'; import PageHeader from '../../components/common/PageHeader'; +import Lightbox from 'yet-another-react-lightbox'; +import 'yet-another-react-lightbox/styles.css'; export default function Images() { const toast = useToast(); @@ -85,6 +88,11 @@ export default function Images() { const [statusUpdateRecordName, setStatusUpdateRecordName] = useState(''); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + // Lightbox state + const [lightboxOpen, setLightboxOpen] = useState(false); + const [lightboxIndex, setLightboxIndex] = useState(0); + const [lightboxSlides, setLightboxSlides] = useState>([]); + // Load images - wrapped in useCallback const loadImages = useCallback(async () => { setLoading(true); @@ -353,6 +361,87 @@ export default function Images() { } }, [toast, images, buildImageQueue]); + // Helper function to convert image_path to web-accessible URL + const getImageUrl = useCallback((image: ContentImage | null): string | null => { + if (!image || !image.image_path) return null; + + // Check if image_path is a valid local file path (not a URL) + const isValidLocalPath = (imagePath: string): boolean => { + if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { + return false; + } + return imagePath.includes('ai-images'); + }; + + if (!isValidLocalPath(image.image_path)) return null; + + // Convert local file path to web-accessible URL + if (image.image_path.includes('ai-images')) { + const filename = image.image_path.split('ai-images/')[1] || image.image_path.split('ai-images\\')[1]; + if (filename) { + return `/images/ai-images/${filename}`; + } + } + + if (image.image_path.startsWith('/images/')) { + return image.image_path; + } + + const filename = image.image_path.split('/').pop() || image.image_path.split('\\').pop(); + return filename ? `/images/ai-images/${filename}` : null; + }, []); + + // Handle image click - open lightbox with all images from the same content + const handleImageClick = useCallback((contentId: number, imageType: 'featured' | 'in_article', position?: number) => { + const contentGroup = images.find(g => g.content_id === contentId); + if (!contentGroup) return; + + // Collect all generated images from this content + const slides: Array<{ src: string; alt?: string }> = []; + let startIndex = 0; + + // Add featured image first (if it exists and is generated) + if (contentGroup.featured_image && contentGroup.featured_image.status === 'generated') { + const url = getImageUrl(contentGroup.featured_image); + if (url) { + slides.push({ + src: url, + alt: contentGroup.featured_image.prompt || 'Featured image', + }); + // If clicked image is featured, start at index 0 + if (imageType === 'featured') { + startIndex = 0; + } + } + } + + // Add in-article images in order + const sortedInArticle = [...contentGroup.in_article_images] + .filter(img => img.status === 'generated') + .sort((a, b) => (a.position || 0) - (b.position || 0)); + + sortedInArticle.forEach((img) => { + const url = getImageUrl(img); + if (url) { + const currentIndex = slides.length; + slides.push({ + src: url, + alt: img.prompt || `In-article image ${img.position}`, + }); + // If clicked image is this in-article image, set start index + if (imageType === 'in_article' && img.position === position) { + startIndex = currentIndex; + } + } + }); + + if (slides.length > 0) { + setLightboxSlides(slides); + setLightboxIndex(startIndex); + setLightboxOpen(true); + } + }, [images, getImageUrl]); + // Get max in-article images from the data (to determine column count) const maxInArticleImages = useMemo(() => { if (images.length === 0) return 5; // Default @@ -370,8 +459,9 @@ export default function Images() { setCurrentPage, maxInArticleImages, onGenerateImages: handleGenerateImages, + onImageClick: handleImageClick, }); - }, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages]); + }, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages, handleImageClick]); // Calculate header metrics const headerMetrics = useMemo(() => { @@ -479,6 +569,15 @@ export default function Images() { isLoading={isUpdatingStatus} /> + {/* Lightbox */} + setLightboxOpen(false)} + index={lightboxIndex} + slides={lightboxSlides} + on={{ view: ({ index }) => setLightboxIndex(index) }} + /> + {/* AI Function Logs - Display below table (only when Resource Debug is enabled) */} {resourceDebugEnabled && aiLogs.length > 0 && (