@@ -20,10 +20,9 @@ export interface ContentImageData {
|
|||||||
interface ContentImageCellProps {
|
interface ContentImageCellProps {
|
||||||
image: ContentImageData | null;
|
image: ContentImageData | null;
|
||||||
maxPromptLength?: number;
|
maxPromptLength?: number;
|
||||||
onImageClick?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContentImageCell({ image, maxPromptLength = 100, onImageClick }: ContentImageCellProps) {
|
export default function ContentImageCell({ image, maxPromptLength = 100 }: ContentImageCellProps) {
|
||||||
const [showFullPrompt, setShowFullPrompt] = useState(false);
|
const [showFullPrompt, setShowFullPrompt] = useState(false);
|
||||||
|
|
||||||
// Check if image_path is a valid local file path (not a URL)
|
// Check if image_path is a valid local file path (not a URL)
|
||||||
@@ -114,14 +113,7 @@ export default function ContentImageCell({ image, maxPromptLength = 100, onImage
|
|||||||
<img
|
<img
|
||||||
src={getLocalImageUrl(image.image_path)}
|
src={getLocalImageUrl(image.image_path)}
|
||||||
alt={prompt || 'Generated image'}
|
alt={prompt || 'Generated image'}
|
||||||
className={`w-full h-24 object-cover rounded border border-gray-300 dark:border-gray-600 ${
|
className="w-full h-24 object-cover rounded border border-gray-300 dark:border-gray-600"
|
||||||
onImageClick ? 'cursor-pointer hover:opacity-80 transition-opacity' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (onImageClick) {
|
|
||||||
onImageClick();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// Show empty placeholder if local image fails to load
|
// Show empty placeholder if local image fails to load
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ export const createImagesPageConfig = (
|
|||||||
setCurrentPage: (page: number) => void;
|
setCurrentPage: (page: number) => void;
|
||||||
maxInArticleImages?: number; // Optional: max in-article images to display
|
maxInArticleImages?: number; // Optional: max in-article images to display
|
||||||
onGenerateImages?: (contentId: number) => void; // Handler for generate images button
|
onGenerateImages?: (contentId: number) => void; // Handler for generate images button
|
||||||
onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void; // Handler for image click
|
|
||||||
}
|
}
|
||||||
): ImagesPageConfig => {
|
): ImagesPageConfig => {
|
||||||
const maxImages = handlers.maxInArticleImages || 5; // Default to 5 in-article images
|
const maxImages = handlers.maxInArticleImages || 5; // Default to 5 in-article images
|
||||||
@@ -85,10 +84,7 @@ export const createImagesPageConfig = (
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
width: '200px',
|
width: '200px',
|
||||||
render: (_value: any, row: ContentImagesGroup) => (
|
render: (_value: any, row: ContentImagesGroup) => (
|
||||||
<ContentImageCell
|
<ContentImageCell image={row.featured_image} />
|
||||||
image={row.featured_image}
|
|
||||||
onImageClick={handlers.onImageClick ? () => handlers.onImageClick!(row.content_id, 'featured') : undefined}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -102,12 +98,7 @@ export const createImagesPageConfig = (
|
|||||||
width: '200px',
|
width: '200px',
|
||||||
render: (_value: any, row: ContentImagesGroup) => {
|
render: (_value: any, row: ContentImagesGroup) => {
|
||||||
const image = row.in_article_images.find(img => img.position === i);
|
const image = row.in_article_images.find(img => img.position === i);
|
||||||
return (
|
return <ContentImageCell image={image || null} />;
|
||||||
<ContentImageCell
|
|
||||||
image={image || null}
|
|
||||||
onImageClick={handlers.onImageClick && image ? () => handlers.onImageClick!(row.content_id, 'in_article', i) : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
fetchImageGenerationSettings,
|
fetchImageGenerationSettings,
|
||||||
generateImages,
|
generateImages,
|
||||||
bulkUpdateImagesStatus,
|
bulkUpdateImagesStatus,
|
||||||
ContentImage,
|
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { FileIcon, DownloadIcon, BoltIcon } from '../../icons';
|
import { FileIcon, DownloadIcon, BoltIcon } from '../../icons';
|
||||||
@@ -21,8 +20,6 @@ import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQu
|
|||||||
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
|
import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordStatusUpdateModal';
|
||||||
import { useResourceDebug } from '../../hooks/useResourceDebug';
|
import { useResourceDebug } from '../../hooks/useResourceDebug';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import Lightbox from 'yet-another-react-lightbox';
|
|
||||||
import 'yet-another-react-lightbox/styles.css';
|
|
||||||
|
|
||||||
export default function Images() {
|
export default function Images() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -88,11 +85,6 @@ export default function Images() {
|
|||||||
const [statusUpdateRecordName, setStatusUpdateRecordName] = useState<string>('');
|
const [statusUpdateRecordName, setStatusUpdateRecordName] = useState<string>('');
|
||||||
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
||||||
|
|
||||||
// Lightbox state
|
|
||||||
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
||||||
const [lightboxIndex, setLightboxIndex] = useState(0);
|
|
||||||
const [lightboxSlides, setLightboxSlides] = useState<Array<{ src: string; alt?: string }>>([]);
|
|
||||||
|
|
||||||
// Load images - wrapped in useCallback
|
// Load images - wrapped in useCallback
|
||||||
const loadImages = useCallback(async () => {
|
const loadImages = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -361,87 +353,6 @@ export default function Images() {
|
|||||||
}
|
}
|
||||||
}, [toast, images, buildImageQueue]);
|
}, [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)
|
// Get max in-article images from the data (to determine column count)
|
||||||
const maxInArticleImages = useMemo(() => {
|
const maxInArticleImages = useMemo(() => {
|
||||||
if (images.length === 0) return 5; // Default
|
if (images.length === 0) return 5; // Default
|
||||||
@@ -459,9 +370,8 @@ export default function Images() {
|
|||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
maxInArticleImages,
|
maxInArticleImages,
|
||||||
onGenerateImages: handleGenerateImages,
|
onGenerateImages: handleGenerateImages,
|
||||||
onImageClick: handleImageClick,
|
|
||||||
});
|
});
|
||||||
}, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages, handleImageClick]);
|
}, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages]);
|
||||||
|
|
||||||
// Calculate header metrics
|
// Calculate header metrics
|
||||||
const headerMetrics = useMemo(() => {
|
const headerMetrics = useMemo(() => {
|
||||||
@@ -569,15 +479,6 @@ export default function Images() {
|
|||||||
isLoading={isUpdatingStatus}
|
isLoading={isUpdatingStatus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Lightbox */}
|
|
||||||
<Lightbox
|
|
||||||
open={lightboxOpen}
|
|
||||||
close={() => setLightboxOpen(false)}
|
|
||||||
index={lightboxIndex}
|
|
||||||
slides={lightboxSlides}
|
|
||||||
on={{ view: ({ index }) => setLightboxIndex(index) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* AI Function Logs - Display below table (only when Resource Debug is enabled) */}
|
{/* AI Function Logs - Display below table (only when Resource Debug is enabled) */}
|
||||||
{resourceDebugEnabled && aiLogs.length > 0 && (
|
{resourceDebugEnabled && aiLogs.length > 0 && (
|
||||||
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user