/** * Images Page - Built with TablePageTemplate * Shows content images grouped by content - one row per content */ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContentImages, ContentImagesGroup, ContentImagesResponse, fetchImageGenerationSettings, generateImages, bulkUpdateImagesStatus, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon } from '../../icons'; import { createImagesPageConfig } from '../../config/pages/images.config'; import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal'; import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal'; import { useResourceDebug } from '../../hooks/useResourceDebug'; export default function Images() { const toast = useToast(); // Resource Debug toggle - controls AI Function Logs const resourceDebugEnabled = useResourceDebug(); // AI Function Logs state const [aiLogs, setAiLogs] = useState>([]); // Helper function to add log entry (only if Resource Debug is enabled) const addAiLog = useCallback((log: { timestamp: string; type: 'request' | 'success' | 'error' | 'step'; action: string; data: any; stepName?: string; percentage?: number; }) => { if (resourceDebugEnabled) { setAiLogs(prev => [...prev, log]); } }, [resourceDebugEnabled]); // Data state const [images, setImages] = useState([]); const [loading, setLoading] = useState(true); // Filter state const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [selectedIds, setSelectedIds] = useState([]); // Pagination state (client-side for now) const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); const pageSize = 10; // Sorting state const [sortBy, setSortBy] = useState('content_title'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [showContent, setShowContent] = useState(false); // Image queue modal state const [isQueueModalOpen, setIsQueueModalOpen] = useState(false); const [imageQueue, setImageQueue] = useState([]); const [currentContentId, setCurrentContentId] = useState(null); const [taskId, setTaskId] = useState(null); const [imageModel, setImageModel] = useState(null); const [imageProvider, setImageProvider] = useState(null); // Status update modal state const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); const [statusUpdateContentId, setStatusUpdateContentId] = useState(null); const [statusUpdateRecordName, setStatusUpdateRecordName] = useState(''); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); // Load images - wrapped in useCallback const loadImages = useCallback(async () => { setLoading(true); setShowContent(false); try { const data: ContentImagesResponse = await fetchContentImages(); let filteredResults = data.results || []; // Client-side search filter if (searchTerm) { filteredResults = filteredResults.filter(group => group.content_title?.toLowerCase().includes(searchTerm.toLowerCase()) ); } // Client-side status filter if (statusFilter) { filteredResults = filteredResults.filter(group => group.overall_status === statusFilter ); } // Client-side sorting filteredResults.sort((a, b) => { let aVal: any = a.content_title; let bVal: any = b.content_title; if (sortBy === 'overall_status') { aVal = a.overall_status; bVal = b.overall_status; } if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; return 0; }); // Client-side pagination const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedResults = filteredResults.slice(startIndex, endIndex); setImages(paginatedResults); setTotalCount(filteredResults.length); setTotalPages(Math.ceil(filteredResults.length / pageSize)); setTimeout(() => { setShowContent(true); setLoading(false); }, 100); } catch (error: any) { console.error('Error loading images:', error); toast.error(`Failed to load images: ${error.message}`); setShowContent(true); setLoading(false); } }, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, toast]); useEffect(() => { loadImages(); }, [loadImages]); // Debounced search useEffect(() => { const timer = setTimeout(() => { if (currentPage === 1) { loadImages(); } else { setCurrentPage(1); } }, 500); return () => clearTimeout(timer); }, [searchTerm, currentPage, loadImages]); // Handle sorting const handleSort = (field: string, direction: 'asc' | 'desc') => { setSortBy(field || 'content_title'); setSortDirection(direction); setCurrentPage(1); }; // Bulk export handler const handleBulkExport = useCallback(async (ids: string[]) => { try { if (!ids || ids.length === 0) { throw new Error('No records selected for export'); } toast.info('Export functionality coming soon'); } catch (error: any) { throw error; } }, [toast]); // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { toast.info(`Bulk action "${action}" for ${ids.length} items`); }, [toast]); // Row action handler const handleRowAction = useCallback(async (action: string, row: ContentImagesGroup) => { if (action === 'update_status') { setStatusUpdateContentId(row.content_id); setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`); setIsStatusModalOpen(true); } }, []); // Handle status update confirmation const handleStatusUpdate = useCallback(async (status: string) => { if (!statusUpdateContentId) return; setIsUpdatingStatus(true); try { const result = await bulkUpdateImagesStatus(statusUpdateContentId, status); toast.success(`Successfully updated ${result.updated_count} image(s) status to ${status}`); setIsStatusModalOpen(false); setStatusUpdateContentId(null); setStatusUpdateRecordName(''); // Reload images to reflect the changes loadImages(); } catch (error: any) { toast.error(`Failed to update status: ${error.message}`); } finally { setIsUpdatingStatus(false); } }, [statusUpdateContentId, toast, loadImages]); // Build image queue structure const buildImageQueue = useCallback((contentId: number, maxInArticleImages: number) => { const contentImages = images.find(g => g.content_id === contentId); if (!contentImages) return []; const queue: ImageQueueItem[] = []; let queueIndex = 1; // Featured image (always first) if (contentImages.featured_image?.status === 'pending' && contentImages.featured_image?.prompt) { queue.push({ imageId: contentImages.featured_image.id || null, index: queueIndex++, label: 'Featured Image', type: 'featured', contentTitle: contentImages.content_title || `Content #${contentId}`, status: 'pending', progress: 0, imageUrl: null, error: null, }); } // In-article images (up to max_in_article_images) const pendingInArticle = contentImages.in_article_images .filter(img => img.status === 'pending' && img.prompt) .slice(0, maxInArticleImages) .sort((a, b) => (a.position || 0) - (b.position || 0)); pendingInArticle.forEach((img, idx) => { queue.push({ imageId: img.id || null, index: queueIndex++, label: `In-Article Image ${img.position || idx + 1}`, type: 'in_article', position: img.position || idx + 1, contentTitle: contentImages.content_title || `Content #${contentId}`, status: 'pending', progress: 0, imageUrl: null, error: null, }); }); return queue; }, [images]); // Generate images handler - Stage 1: Open modal immediately const handleGenerateImages = useCallback(async (contentId: number) => { try { // Get content images const contentImages = images.find(g => g.content_id === contentId); if (!contentImages) { toast.error('Content not found'); return; } // Fetch image generation settings to get max_in_article_images, model, and provider let maxInArticleImages = 2; // Default let model = null; let provider = null; try { const settings = await fetchImageGenerationSettings(); if (settings.success && settings.config) { maxInArticleImages = settings.config.max_in_article_images || 2; model = settings.config.model || null; provider = settings.config.provider || null; } } catch (error) { console.warn('Failed to fetch image settings, using default:', error); } // Store model and provider for modal display setImageModel(model); setImageProvider(provider); // Build image queue const queue = buildImageQueue(contentId, maxInArticleImages); if (queue.length === 0) { toast.info('No pending images with prompts found for this content'); return; } // STAGE 1: Open modal immediately with all progress bars setImageQueue(queue); setCurrentContentId(contentId); setIsQueueModalOpen(true); // Collect image IDs for API call const imageIds: number[] = queue .map(item => item.imageId) .filter((id): id is number => id !== null); console.log('[Generate Images] Stage 1 Complete: Modal opened with', queue.length, 'images'); console.log('[Generate Images] Image IDs to generate:', imageIds); console.log('[Generate Images] Max in-article images from settings:', maxInArticleImages); // STAGE 2: Start actual generation addAiLog({ timestamp: new Date().toISOString(), type: 'request', action: 'generate_images', data: { imageIds, contentId, totalImages: imageIds.length } }); const result = await generateImages(imageIds, contentId); if (result.success && result.task_id) { // Task started successfully - polling will be handled by ImageQueueModal setTaskId(result.task_id); console.log('[Generate Images] Stage 2: Task started with ID:', result.task_id); addAiLog({ timestamp: new Date().toISOString(), type: 'step', action: 'generate_images', stepName: 'Task Queued', data: { task_id: result.task_id, message: 'Image generation task queued' } }); } else { toast.error(result.error || 'Failed to start image generation'); setIsQueueModalOpen(false); setTaskId(null); addAiLog({ timestamp: new Date().toISOString(), type: 'error', action: 'generate_images', data: { error: result.error || 'Failed to start image generation' } }); } } catch (error: any) { console.error('[Generate Images] Exception:', error); toast.error(`Failed to initialize image generation: ${error.message}`); } }, [toast, images, buildImageQueue]); // Get max in-article images from the data (to determine column count) const maxInArticleImages = useMemo(() => { if (images.length === 0) return 5; // Default const max = Math.max(...images.map(group => group.in_article_images.length)); return Math.max(max, 5); // At least 5 columns }, [images]); // Create page config const pageConfig = useMemo(() => { return createImagesPageConfig({ searchTerm, setSearchTerm, statusFilter, setStatusFilter, setCurrentPage, maxInArticleImages, onGenerateImages: handleGenerateImages, }); }, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages]); // Calculate header metrics const headerMetrics = useMemo(() => { if (!pageConfig?.headerMetrics) return []; return pageConfig.headerMetrics.map((metric) => ({ label: metric.label, value: metric.calculate({ images, totalCount }), accentColor: metric.accentColor, })); }, [pageConfig?.headerMetrics, images, totalCount]); return ( <> } subtitle="Manage images for content articles" columns={pageConfig.columns} data={images} loading={loading} showContent={showContent} filters={pageConfig.filters} filterValues={{ search: searchTerm, status: statusFilter, }} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { setSearchTerm(stringValue); } else if (key === 'status') { setStatusFilter(stringValue); } setCurrentPage(1); }} onBulkExport={handleBulkExport} onBulkAction={handleBulkAction} getItemDisplayName={(row: ContentImagesGroup) => row.content_title || `Content #${row.content_id}`} onExport={async () => { toast.info('Export functionality coming soon'); }} onExportIcon={} selectionLabel="content" pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} selection={{ selectedIds, onSelectionChange: setSelectedIds, }} sorting={{ sortBy, sortDirection, onSort: handleSort, }} headerMetrics={headerMetrics} onFilterReset={() => { setSearchTerm(''); setStatusFilter(''); setCurrentPage(1); }} onRowAction={handleRowAction} /> { setIsQueueModalOpen(false); setImageQueue([]); setCurrentContentId(null); setTaskId(null); setImageModel(null); setImageProvider(null); // Reload images after closing if generation completed loadImages(); }} queue={imageQueue} totalImages={imageQueue.length} taskId={taskId} model={imageModel || undefined} provider={imageProvider || undefined} onUpdateQueue={setImageQueue} onLog={addAiLog} /> {/* Status Update Modal */} { setIsStatusModalOpen(false); setStatusUpdateContentId(null); setStatusUpdateRecordName(''); }} onConfirm={handleStatusUpdate} title="Update Image Status" recordName={statusUpdateRecordName} statusOptions={[ { value: 'pending', label: 'Pending' }, { value: 'generated', label: 'Generated' }, { value: 'failed', label: 'Failed' }, ]} isLoading={isUpdatingStatus} /> {/* AI Function Logs - Display below table (only when Resource Debug is enabled) */} {resourceDebugEnabled && aiLogs.length > 0 && (

AI Function Logs

{aiLogs.slice().reverse().map((log, index) => (
[{log.type.toUpperCase()}] {log.action} {log.stepName && ( {log.stepName} )} {log.percentage !== undefined && ( {log.percentage}% )}
{new Date(log.timestamp).toLocaleTimeString()}
                  {JSON.stringify(log.data, null, 2)}
                
))}
)} ); }