From b0c14ccc327977008e94b43a34a41322a4759ac9 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Thu, 25 Dec 2025 04:06:19 +0000 Subject: [PATCH] content view template final version --- .../ai/functions/generate_image_prompts.py | 4 +- .../src/templates/ContentViewTemplate.tsx | 174 ++++++++++++++++-- 2 files changed, 159 insertions(+), 19 deletions(-) diff --git a/backend/igny8_core/ai/functions/generate_image_prompts.py b/backend/igny8_core/ai/functions/generate_image_prompts.py index d9d12f2b..3474075f 100644 --- a/backend/igny8_core/ai/functions/generate_image_prompts.py +++ b/backend/igny8_core/ai/functions/generate_image_prompts.py @@ -197,12 +197,12 @@ class GenerateImagePromptsFunction(BaseAIFunction): prompt_text = str(prompt_data) caption_text = '' - heading = h2_headings[idx] if idx < len(h2_headings) else f"Section {idx + 1}" + heading = h2_headings[idx] if idx < len(h2_headings) else f"Section {idx}" Images.objects.update_or_create( content=content, image_type='in_article', - position=idx + 1, + position=idx, # 0-based position matching section array indices defaults={ 'prompt': prompt_text, 'caption': caption_text, diff --git a/frontend/src/templates/ContentViewTemplate.tsx b/frontend/src/templates/ContentViewTemplate.tsx index 56e9ee9b..7d62d6ed 100644 --- a/frontend/src/templates/ContentViewTemplate.tsx +++ b/frontend/src/templates/ContentViewTemplate.tsx @@ -345,19 +345,76 @@ const IntroBlock = ({ html }: { html: string }) => ( ); +// Helper to split content at first H3 tag +const splitAtFirstH3 = (html: string): { beforeH3: string; h3AndAfter: string } => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const h3 = doc.querySelector('h3'); + + if (!h3) { + return { beforeH3: html, h3AndAfter: '' }; + } + + const beforeNodes: Node[] = []; + const afterNodes: Node[] = []; + let foundH3 = false; + + Array.from(doc.body.childNodes).forEach((node) => { + if (node === h3) { + foundH3 = true; + afterNodes.push(node); + } else if (foundH3) { + afterNodes.push(node); + } else { + beforeNodes.push(node); + } + }); + + const serializeNodes = (nodes: Node[]): string => + nodes + .map((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + return (node as HTMLElement).outerHTML; + } + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent ?? ''; + } + return ''; + }) + .join(''); + + return { + beforeH3: serializeNodes(beforeNodes), + h3AndAfter: serializeNodes(afterNodes), + }; +}; + +// Helper to check if section contains a table +const hasTable = (html: string): boolean => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + return doc.querySelector('table') !== null; +}; + const ContentSectionBlock = ({ section, image, loading, index, + imagePlacement = 'right', + firstImage = null, }: { section: ArticleSection; image: ImageRecord | null; loading: boolean; index: number; + imagePlacement?: 'left' | 'center' | 'right'; + firstImage?: ImageRecord | null; }) => { const hasImage = Boolean(image); const headingLabel = section.heading || `Section ${index + 1}`; + const sectionHasTable = hasTable(section.bodyHtml); + const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml); return (
@@ -377,16 +434,86 @@ const ContentSectionBlock = ({ -
-
-
-
- {hasImage && ( -
- + {imagePlacement === 'center' && hasImage ? ( +
+ {/* Content before H3 */} + {beforeH3 && ( +
+
+
+ )} + + {/* Centered image before H3 */} +
+
+ +
- )} -
+ + {/* H3 and remaining content */} + {h3AndAfter && ( +
+
+
+ )} + + {/* Fallback if no H3 found */} + {!beforeH3 && !h3AndAfter && ( +
+
+
+ )} +
+ ) : sectionHasTable && hasImage && firstImage ? ( +
+ {/* Content before H3 */} + {beforeH3 && ( +
+
+
+ )} + + {/* Two images side by side at 50% width each */} +
+
+ +
+
+ +
+
+ + {/* H3 and remaining content */} + {h3AndAfter && ( +
+
+
+ )} + + {/* Fallback if no H3 found */} + {!beforeH3 && !h3AndAfter && ( +
+
+
+ )} +
+ ) : ( +
+ {imagePlacement === 'left' && hasImage && ( +
+ +
+ )} +
+
+
+ {imagePlacement === 'right' && hasImage && ( +
+ +
+ )} +
+ )}
@@ -404,6 +531,14 @@ interface ArticleBodyProps { const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => { const hasStructuredSections = sections.length > 0; + // Calculate image placement: right → center → left → repeat + const getImagePlacement = (index: number): 'left' | 'center' | 'right' => { + const position = index % 3; + if (position === 0) return 'right'; + if (position === 1) return 'center'; + return 'left'; + }; + if (!hasStructuredSections && !introHtml && rawHtml) { return (
@@ -414,6 +549,9 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm ); } + // Get the first in-article image (position 0) + const firstImage = sectionImages.length > 0 ? sectionImages[0] : null; + return (
{introHtml && } @@ -424,6 +562,8 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm image={sectionImages[index] ?? null} loading={imagesLoading} index={index} + imagePlacement={getImagePlacement(index)} + firstImage={firstImage} /> ))}
@@ -535,13 +675,13 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten const byPosition = new Map(); sorted.forEach((img, index) => { - const pos = img.position ?? index + 1; + const pos = img.position ?? index; byPosition.set(pos, img); }); const usedPositions = new Set(); const merged: ImageRecord[] = prompts.map((prompt, index) => { - const position = index + 1; + const position = index; // 0-based position matching section array index const existing = byPosition.get(position); usedPositions.add(position); if (existing) { @@ -561,7 +701,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten image_path: undefined, prompt, status: 'pending', - position, + position, // 0-based position created_at: '', updated_at: '', account_id: undefined, @@ -569,7 +709,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten }); sorted.forEach((img, idx) => { - const position = img.position ?? idx + 1; + const position = img.position ?? idx; if (!usedPositions.has(position)) { merged.push(img); } @@ -596,7 +736,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten if (loading) { return (
-
+
@@ -613,7 +753,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten if (!content) { return (
-
+

Content Not Found

@@ -663,7 +803,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten return (
-
+
{/* Back Button */} {onBack && (