From dabaa140a72c8b99323f4a62e285e7b9df4aecd9 Mon Sep 17 00:00:00 2001 From: Desktop Date: Thu, 13 Nov 2025 00:41:30 +0500 Subject: [PATCH] iamge modal --- frontend/src/config/pages/images.config.tsx | 13 +++- frontend/src/pages/Writer/Images.tsx | 82 ++++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) 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..ce5dc000 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,7 @@ 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 { Modal } from '../../components/ui/modal'; export default function Images() { const toast = useToast(); @@ -85,6 +87,10 @@ export default function Images() { const [statusUpdateRecordName, setStatusUpdateRecordName] = useState(''); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + // Image modal state + const [isImageModalOpen, setIsImageModalOpen] = useState(false); + const [modalImageUrl, setModalImageUrl] = useState(null); + // Load images - wrapped in useCallback const loadImages = useCallback(async () => { setLoading(true); @@ -353,6 +359,58 @@ 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 modal with single image + const handleImageClick = useCallback((contentId: number, imageType: 'featured' | 'in_article', position?: number) => { + const contentGroup = images.find(g => g.content_id === contentId); + if (!contentGroup) return; + + let image: ContentImage | null = null; + + if (imageType === 'featured' && contentGroup.featured_image) { + image = contentGroup.featured_image; + } else if (imageType === 'in_article' && position) { + image = contentGroup.in_article_images.find(img => img.position === position) || null; + } + + if (image && image.status === 'generated') { + const url = getImageUrl(image); + if (url) { + setModalImageUrl(url); + setIsImageModalOpen(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 +428,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 +538,27 @@ export default function Images() { isLoading={isUpdatingStatus} /> + {/* Image Modal - 800px wide, auto height */} + { + setIsImageModalOpen(false); + setModalImageUrl(null); + }} + className="max-w-[800px] w-full mx-4" + > + {modalImageUrl && ( +
+ Content image +
+ )} +
+ {/* AI Function Logs - Display below table (only when Resource Debug is enabled) */} {resourceDebugEnabled && aiLogs.length > 0 && (