Image genartiona dn temaplte design redesign

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-10 03:58:02 +00:00
parent ce66dadc00
commit 0c693dc1cc
18 changed files with 1717 additions and 214 deletions

View File

@@ -111,14 +111,17 @@ export const createImagesPageConfig = (
];
// Add in-article image columns dynamically
for (let i = 1; i <= maxImages; i++) {
// Backend uses 0-indexed positions (0, 1, 2, 3)
// Display uses 1-indexed labels (In-Article 1, 2, 3, 4)
for (let i = 0; i < maxImages; i++) {
const displayIndex = i + 1; // 1-indexed for display
columns.push({
key: `in_article_${i}`,
label: `In-Article ${i}`,
key: `in_article_${displayIndex}`,
label: `In-Article ${displayIndex}`,
sortable: false,
width: '150px',
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); // 0-indexed position
return (
<ContentImageCell
image={image || null}

View File

@@ -94,6 +94,11 @@ export default function SiteSettings() {
const [aiSettingsLoading, setAiSettingsLoading] = useState(false);
const [aiSettingsSaving, setAiSettingsSaving] = useState(false);
// Image size information (from model config)
const [featuredImageSize, setFeaturedImageSize] = useState('1792x1024');
const [landscapeImageSize, setLandscapeImageSize] = useState('1792x1024');
const [squareImageSize, setSquareImageSize] = useState('1024x1024');
// Sectors selection state
const [industries, setIndustries] = useState<Industry[]>([]);
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
@@ -364,6 +369,11 @@ export default function SiteSettings() {
setSelectedStyle(response.image_generation.selected_style || 'photorealistic');
setMaxImages(response.image_generation.max_images ?? 4);
setMaxAllowed(response.image_generation.max_allowed ?? 4);
// Set image sizes from model config
setFeaturedImageSize(response.image_generation.featured_image_size || '1792x1024');
setLandscapeImageSize(response.image_generation.landscape_image_size || '1792x1024');
setSquareImageSize(response.image_generation.square_image_size || '1024x1024');
}
} catch (error: any) {
console.error('Error loading AI settings:', error);
@@ -986,6 +996,22 @@ export default function SiteSettings() {
className="w-full"
/>
</div>
{/* Image Sizes Display */}
<div className="grid grid-cols-3 gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
<div className="text-center">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Featured Image</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{featuredImageSize}</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Landscape</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{landscapeImageSize}</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Square</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{squareImageSize}</p>
</div>
</div>
</div>
)}
</Card>

View File

@@ -455,7 +455,8 @@ export default function Images() {
if (imageType === 'featured' && contentGroup.featured_image) {
image = contentGroup.featured_image;
} else if (imageType === 'in_article' && position) {
} else if (imageType === 'in_article' && position !== undefined) {
// Position is 0-indexed, so check for undefined instead of falsy
image = contentGroup.in_article_images.find(img => img.position === position) || null;
}

View File

@@ -295,10 +295,12 @@ const SectionImageBlock = ({
image,
loading,
heading,
showPrompt = true,
}: {
image: ImageRecord | null;
loading: boolean;
heading: string;
showPrompt?: boolean;
}) => {
if (!image && !loading) return null;
@@ -323,7 +325,7 @@ const SectionImageBlock = ({
<ImageStatusPill status={image?.status} />
</div>
</div>
{image?.caption && (
{showPrompt && image?.caption && (
<figcaption className="space-y-3 px-6 py-5 text-sm leading-relaxed text-gray-600 dark:text-gray-300">
<p className="font-semibold uppercase tracking-[0.25em] text-gray-400 dark:text-gray-500">
Image Caption
@@ -391,12 +393,14 @@ const splitAtFirstH3 = (html: string): { beforeH3: string; h3AndAfter: string }
};
/**
* ContentSectionBlock - Renders a content section with image layout based on aspect ratio
* ContentSectionBlock - Renders a content section with image layout based on distribution pattern
*
* Layout rules:
* - Single landscape image: 100% width (full width)
* - Single square image: 50% width (centered)
* - Two square images (paired): Side by side (50% each)
* Layout rules (first 4 sections):
* - Section 1: Square image right-aligned (50%) with description
* - Section 2: Landscape image full-width (1024px) with description
* - Section 3: Square image left-aligned (50%) with description
* - Section 4: Landscape image full-width (1024px) with description
* - Sections 5+: Reuse images without descriptions
*/
const ContentSectionBlock = ({
section,
@@ -404,32 +408,33 @@ const ContentSectionBlock = ({
loading,
index,
aspectRatio = 'square',
pairedSquareImage = null,
imageAlign = 'full',
showDescription = true,
}: {
section: ArticleSection;
image: ImageRecord | null;
loading: boolean;
index: number;
aspectRatio?: 'square' | 'landscape';
pairedSquareImage?: ImageRecord | null;
imageAlign?: 'left' | 'right' | 'full';
showDescription?: boolean;
}) => {
const hasImage = Boolean(image);
const hasPairedImage = Boolean(pairedSquareImage);
const headingLabel = section.heading || `Section ${index + 1}`;
const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml);
// Determine image container width class based on aspect ratio and pairing
// Determine image container width class based on aspect ratio and alignment
const getImageContainerClass = () => {
if (hasPairedImage) {
// Two squares side by side
return 'w-full';
}
if (aspectRatio === 'landscape') {
// Landscape: 100% width
return 'w-full';
return 'w-full max-w-[1024px] mx-auto';
}
// Single square: 50% width centered
return 'w-full max-w-[50%]';
if (imageAlign === 'left') {
return 'w-full max-w-[50%] mr-auto';
}
if (imageAlign === 'right') {
return 'w-full max-w-[50%] ml-auto';
}
return 'w-full max-w-[50%] mx-auto';
};
return (
@@ -460,25 +465,12 @@ const ContentSectionBlock = ({
</div>
)}
{/* Image section - layout depends on aspect ratio */}
{/* Image section - layout depends on aspect ratio and alignment */}
{hasImage && (
<div className="flex justify-center">
{hasPairedImage ? (
// Two squares side by side (50% each)
<div className="grid w-full grid-cols-2 gap-6">
<div className="w-full">
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
</div>
<div className="w-full">
<SectionImageBlock image={pairedSquareImage} loading={loading} heading={`${headingLabel} (2)`} />
</div>
</div>
) : (
// Single image with width based on aspect ratio
<div className={getImageContainerClass()}>
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
</div>
)}
<div className={getImageContainerClass()}>
<SectionImageBlock image={image} loading={loading} heading={headingLabel} showPrompt={showDescription} />
</div>
</div>
)}
@@ -513,21 +505,22 @@ interface ArticleBodyProps {
const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => {
const hasStructuredSections = sections.length > 0;
// Determine image aspect ratio from record or fallback to position-based calculation
// Position 0, 2 = square (1024x1024), Position 1, 3 = landscape (model-specific)
const getImageAspectRatio = (image: ImageRecord | null, index: number): 'square' | 'landscape' => {
if (image?.aspect_ratio) return image.aspect_ratio;
// Fallback: even positions (0, 2) are square, odd positions (1, 3) are landscape
return index % 2 === 0 ? 'square' : 'landscape';
};
// Image distribution mapping for first 4 sections (matches WordPress template)
const imageDistribution = [
{ position: 0, type: 'square' as const, align: 'right' as const }, // Section 1
{ position: 3, type: 'landscape' as const, align: 'full' as const }, // Section 2
{ position: 2, type: 'square' as const, align: 'left' as const }, // Section 3
{ position: 1, type: 'landscape' as const, align: 'full' as const }, // Section 4
];
// Check if two consecutive images are both squares (for side-by-side layout)
const getNextSquareImage = (currentIndex: number): ImageRecord | null => {
const nextImage = sectionImages[currentIndex + 1];
if (nextImage && getImageAspectRatio(nextImage, currentIndex + 1) === 'square') {
return nextImage;
}
return null;
// Reuse pattern for sections 5+ (without descriptions)
const reusePattern = [1, 0, 3, 2];
// Get image aspect ratio from record or fallback to position-based calculation
const getImageAspectRatio = (image: ImageRecord | null, position: number): 'square' | 'landscape' => {
if (image?.aspect_ratio) return image.aspect_ratio;
// Fallback: positions 0, 2 are square, positions 1, 3 are landscape
return position === 0 || position === 2 ? 'square' : 'landscape';
};
if (!hasStructuredSections && !introHtml && rawHtml) {
@@ -540,42 +533,46 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm
);
}
// Get the first in-article image (position 0)
const firstImage = sectionImages.length > 0 ? sectionImages[0] : null;
// Track which images have been rendered as pairs (to skip the second in the pair)
const renderedPairIndices = new Set<number>();
return (
<div className="space-y-12">
{introHtml && <IntroBlock html={introHtml} />}
{sections.map((section, index) => {
// Skip if this image was already rendered as part of a pair
if (renderedPairIndices.has(index)) {
return null;
}
{sections.map((section, sectionIndex) => {
let image: ImageRecord | null = null;
let aspectRatio: 'square' | 'landscape' = 'landscape';
let imageAlign: 'left' | 'right' | 'full' = 'full';
let showDescription = true;
const currentImage = sectionImages[index] ?? null;
const currentAspectRatio = getImageAspectRatio(currentImage, index);
// Check if current is square and next is also square for side-by-side layout
let pairedSquareImage: ImageRecord | null = null;
if (currentAspectRatio === 'square') {
pairedSquareImage = getNextSquareImage(index);
if (pairedSquareImage) {
renderedPairIndices.add(index + 1); // Mark next as rendered
// First 4 sections: use distribution pattern
if (sectionIndex < 4) {
const dist = imageDistribution[sectionIndex];
const imgPosition = dist.position;
image = sectionImages[imgPosition] ?? null;
aspectRatio = dist.type;
imageAlign = dist.align;
showDescription = true;
}
// Sections 5+: reuse images without descriptions
else {
const reuseIndex = (sectionIndex - 4) % reusePattern.length;
const imgPosition = reusePattern[reuseIndex];
image = sectionImages[imgPosition] ?? null;
if (image) {
aspectRatio = getImageAspectRatio(image, imgPosition);
imageAlign = (aspectRatio === 'square') ? (reuseIndex % 2 === 0 ? 'right' : 'left') : 'full';
}
showDescription = false;
}
return (
<ContentSectionBlock
key={section.id || `section-${index}`}
key={section.id || `section-${sectionIndex}`}
section={section}
image={currentImage}
image={image}
loading={imagesLoading}
index={index}
aspectRatio={currentAspectRatio}
pairedSquareImage={pairedSquareImage}
index={sectionIndex}
aspectRatio={aspectRatio}
imageAlign={imageAlign}
showDescription={showDescription}
/>
);
})}
@@ -749,7 +746,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
if (loading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-6"></div>
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded w-3/4 mb-4"></div>
@@ -766,7 +763,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
if (!content) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-8 text-center">
<XCircleIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
@@ -834,7 +831,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
{/* Back Button */}
{onBack && (
<Button
@@ -1103,7 +1100,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
{/* Featured Image */}
{shouldShowFeaturedBlock && (
<div className="mb-12 max-w-[800px] mx-auto">
<div className="mb-12 max-w-[1024px] mx-auto">
<FeaturedImageBlock image={resolvedFeaturedImage} loading={imagesLoading} />
</div>
)}