Add generate_page_content functionality for structured page content generation

- Introduced a new AI function `generate_page_content` to create structured content for website pages using JSON blocks.
- Updated `AIEngine` to handle the new function and return appropriate messages for content generation.
- Enhanced `PageGenerationService` to utilize the new AI function for generating page content based on blueprints.
- Modified `prompts.py` to include detailed content generation requirements for the new function.
- Updated site rendering logic to accommodate structured content blocks in various layouts.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-18 23:30:20 +00:00
parent 6c6133a683
commit e4e7ddfdf3
13 changed files with 1283 additions and 100 deletions

View File

@@ -41,36 +41,32 @@ function SiteRenderer() {
}
// Build navigation from site definition
// Show pages that are published, ready, or in navigation (excluding home and draft/generating)
const navigation = siteDefinition.navigation || siteDefinition.pages
.filter(p =>
p.slug !== 'home' &&
(p.status === 'published' || p.status === 'ready' || siteDefinition.navigation?.some(n => n.slug === p.slug))
)
.sort((a, b) => {
// Try to get order from navigation or use page order
const navA = siteDefinition.navigation?.find(n => n.slug === a.slug);
const navB = siteDefinition.navigation?.find(n => n.slug === b.slug);
return (navA?.order ?? a.order ?? 0) - (navB?.order ?? b.order ?? 0);
})
.map(page => ({
label: page.title,
slug: page.slug,
order: page.order || 0
}));
// Show all published/ready pages (excluding home and draft/generating)
// Use explicit navigation if available, otherwise auto-generate from pages
const navigation = siteDefinition.navigation && siteDefinition.navigation.length > 0
? siteDefinition.navigation
: siteDefinition.pages
.filter(p =>
p.slug !== 'home' &&
(p.status === 'published' || p.status === 'ready') &&
p.status !== 'draft' &&
p.status !== 'generating'
)
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map(page => ({
label: page.title,
slug: page.slug,
order: page.order || 0
}));
// Filter pages based on current route
const currentPageSlug = pageSlug || 'home';
const currentPage = siteDefinition.pages.find(p => p.slug === currentPageSlug);
// If specific page requested, show only that page; otherwise show all published/ready pages
const pagesToRender = currentPageSlug && currentPageSlug !== 'home' && currentPage
// Show only the current page (home page on home route, specific page on page route)
const pagesToRender = currentPage
? [currentPage]
: siteDefinition.pages.filter(p =>
p.status === 'published' ||
p.status === 'ready' ||
(p.slug === 'home' && p.status !== 'draft' && p.status !== 'generating')
);
: []; // Fallback: no page found
return (
<div className="site-renderer" style={{

View File

@@ -7,6 +7,7 @@
import React from 'react';
import type { SiteDefinition } from '../types';
import { renderTemplate } from './templateEngine';
import { renderPageByType } from './pageTypeRenderer';
import {
DefaultLayout,
MinimalLayout,
@@ -65,28 +66,20 @@ function renderDefaultLayout(siteDefinition: SiteDefinition): React.ReactElement
const showHero = isHomePage || (homePage && siteDefinition.pages.length > 1);
const hero: React.ReactNode = (showHero && heroBlock) ? (renderTemplate(heroBlock) as React.ReactNode) : undefined;
// Render all pages as sections (excluding hero from home page if it exists)
// Render all pages using page-type-specific templates
const sections = siteDefinition.pages
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => {
// Filter out hero block if it's the home page (already rendered as hero)
// Use page-type-specific renderer if available
const blocksToRender = page.slug === 'home' && heroBlock && showHero
? page.blocks?.filter(b => b.type !== 'hero') || []
: page.blocks || [];
// Render using page-type template
return (
<div key={page.id} className="page" data-page-slug={page.slug} style={{ textAlign: 'center' }}>
{page.slug !== 'home' && <h2 style={{ textAlign: 'center', marginBottom: '1.5rem' }}>{page.title}</h2>}
{blocksToRender.length > 0 ? (
blocksToRender.map((block, index) => (
<div key={index} className="block" data-block-type={block.type} style={{ textAlign: 'center' }}>
{renderTemplate(block)}
</div>
))
) : page.slug !== 'home' ? (
<p>No content available for this page.</p>
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, blocksToRender)}
</div>
);
});
@@ -107,17 +100,11 @@ function renderMinimalLayout(siteDefinition: SiteDefinition): React.ReactElement
const mainContent = (
<>
{siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
<h2>{page.title}</h2>
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, page.blocks || [])}
</div>
))}
</>
@@ -138,16 +125,11 @@ function renderMagazineLayout(siteDefinition: SiteDefinition): React.ReactElemen
const mainContent = (
<>
{siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, page.blocks || [])}
</div>
))}
</>
@@ -168,16 +150,11 @@ function renderEcommerceLayout(siteDefinition: SiteDefinition): React.ReactEleme
const mainContent = (
<>
{siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, page.blocks || [])}
</div>
))}
</>
@@ -198,16 +175,11 @@ function renderPortfolioLayout(siteDefinition: SiteDefinition): React.ReactEleme
const mainContent = (
<>
{siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, page.blocks || [])}
</div>
))}
</>
@@ -228,16 +200,11 @@ function renderBlogLayout(siteDefinition: SiteDefinition): React.ReactElement {
const mainContent = (
<>
{siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, page.blocks || [])}
</div>
))}
</>
@@ -258,16 +225,11 @@ function renderCorporateLayout(siteDefinition: SiteDefinition): React.ReactEleme
const mainContent = (
<>
{siteDefinition.pages
.filter((page) => page.status !== 'draft')
.filter((page) => page.status !== 'draft' && page.status !== 'generating')
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((page) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))
) : null}
<div key={page.id} className="page-wrapper" data-page-slug={page.slug}>
{renderPageByType(page, page.blocks || [])}
</div>
))}
</>

View File

@@ -0,0 +1,374 @@
/**
* Page Type Renderer
* Renders page-type-specific templates for different page types.
*
* Each page type has its own template structure optimized for that content type.
*/
import React from 'react';
import type { PageDefinition, Block } from '../types';
import { renderTemplate } from './templateEngine';
/**
* Render a page based on its type.
* Falls back to default rendering if no specific template exists.
*/
export function renderPageByType(page: PageDefinition, blocks: Block[]): React.ReactElement {
// Determine page type - use page.type if available, otherwise infer from slug
let pageType = page.type;
if (!pageType || pageType === 'custom') {
// Infer type from slug
if (page.slug === 'home') pageType = 'home';
else if (page.slug === 'products') pageType = 'products';
else if (page.slug === 'blog') pageType = 'blog';
else if (page.slug === 'contact') pageType = 'contact';
else if (page.slug === 'about') pageType = 'about';
else if (page.slug === 'services') pageType = 'services';
else pageType = 'custom';
}
switch (pageType) {
case 'home':
return renderHomePage(page, blocks);
case 'products':
return renderProductsPage(page, blocks);
case 'blog':
return renderBlogPage(page, blocks);
case 'contact':
return renderContactPage(page, blocks);
case 'about':
return renderAboutPage(page, blocks);
case 'services':
return renderServicesPage(page, blocks);
case 'custom':
default:
return renderCustomPage(page, blocks);
}
}
/**
* Home Page Template
* Structure: Hero → Features → Testimonials → CTA
*/
function renderHomePage(page: PageDefinition, blocks: Block[]): React.ReactElement {
// Find specific blocks
const heroBlock = blocks.find(b => b.type === 'hero');
const featuresBlock = blocks.find(b => b.type === 'features' || b.type === 'grid');
const testimonialsBlock = blocks.find(b => b.type === 'testimonials');
const ctaBlock = blocks.find(b => b.type === 'cta');
const otherBlocks = blocks.filter(b =>
!['hero', 'features', 'grid', 'testimonials', 'cta'].includes(b.type)
);
return (
<div className="page-home" data-page-slug={page.slug}>
{/* Hero Section */}
{heroBlock && (
<section className="home-hero">
{renderTemplate(heroBlock)}
</section>
)}
{/* Features Section */}
{featuresBlock && (
<section className="home-features" style={{ padding: '3rem 2rem', background: '#f9fafb' }}>
{renderTemplate(featuresBlock)}
</section>
)}
{/* Other Content Blocks */}
{otherBlocks.length > 0 && (
<section className="home-content" style={{ padding: '3rem 2rem' }}>
{otherBlocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))}
</section>
)}
{/* Testimonials Section */}
{testimonialsBlock && (
<section className="home-testimonials" style={{ padding: '3rem 2rem', background: '#f9fafb' }}>
{renderTemplate(testimonialsBlock)}
</section>
)}
{/* CTA Section */}
{ctaBlock && (
<section className="home-cta">
{renderTemplate(ctaBlock)}
</section>
)}
</div>
);
}
/**
* Products Page Template
* Structure: Title → Product Grid → Filters (if available)
*/
function renderProductsPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
const productsBlock = blocks.find(b => b.type === 'products' || b.type === 'grid');
const otherBlocks = blocks.filter(b => b.type !== 'products' && b.type !== 'grid');
return (
<div className="page-products" data-page-slug={page.slug}>
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
{page.title}
</h1>
</header>
{/* Product Grid */}
{productsBlock && (
<section className="products-grid" style={{ padding: '2rem 0' }}>
{renderTemplate(productsBlock)}
</section>
)}
{/* Other Content */}
{otherBlocks.length > 0 && (
<section className="products-content" style={{ padding: '2rem 0' }}>
{otherBlocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))}
</section>
)}
</div>
);
}
/**
* Blog Page Template
* Structure: Title → Post List/Grid → Sidebar (if available)
*/
function renderBlogPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
const heroBlock = blocks.find(b => b.type === 'hero');
const contentBlocks = blocks.filter(b => b.type !== 'hero');
return (
<div className="page-blog" data-page-slug={page.slug}>
{/* Hero/Header */}
{heroBlock ? (
<section className="blog-hero">
{renderTemplate(heroBlock)}
</section>
) : (
<header style={{ marginBottom: '2rem', textAlign: 'center', padding: '2rem 0' }}>
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
{page.title}
</h1>
</header>
)}
{/* Blog Content - Grid Layout for Posts */}
<section className="blog-content" style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '2rem',
padding: '2rem 0'
}}>
{contentBlocks.map((block, index) => (
<article key={index} className="blog-post" style={{
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '1.5rem',
background: '#fff'
}}>
{renderTemplate(block)}
</article>
))}
</section>
</div>
);
}
/**
* Contact Page Template
* Structure: Title → Contact Info → Form → Map (if available)
*/
function renderContactPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
const formBlock = blocks.find(b => b.type === 'form');
const textBlocks = blocks.filter(b => b.type === 'text' || b.type === 'section');
const mapBlock = blocks.find(b => b.type === 'image' && b.data?.caption?.toLowerCase().includes('map'));
const otherBlocks = blocks.filter(b =>
b.type !== 'form' &&
b.type !== 'text' &&
b.type !== 'section' &&
!(b.type === 'image' && b.data?.caption?.toLowerCase().includes('map'))
);
return (
<div className="page-contact" data-page-slug={page.slug}>
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
{page.title}
</h1>
</header>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '2rem',
padding: '2rem 0'
}}>
{/* Contact Information */}
{textBlocks.length > 0 && (
<section className="contact-info">
{textBlocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))}
</section>
)}
{/* Contact Form */}
{formBlock && (
<section className="contact-form">
{renderTemplate(formBlock)}
</section>
)}
</div>
{/* Map */}
{mapBlock && (
<section className="contact-map" style={{ marginTop: '2rem' }}>
{renderTemplate(mapBlock)}
</section>
)}
{/* Other Content */}
{otherBlocks.length > 0 && (
<section className="contact-other" style={{ marginTop: '2rem' }}>
{otherBlocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))}
</section>
)}
</div>
);
}
/**
* About Page Template
* Structure: Hero → Mission → Team/Values → Stats
*/
function renderAboutPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
const heroBlock = blocks.find(b => b.type === 'hero');
const statsBlock = blocks.find(b => b.type === 'stats');
const servicesBlock = blocks.find(b => b.type === 'services' || b.type === 'features');
const otherBlocks = blocks.filter(b =>
!['hero', 'stats', 'services', 'features'].includes(b.type)
);
return (
<div className="page-about" data-page-slug={page.slug}>
{/* Hero Section */}
{heroBlock && (
<section className="about-hero">
{renderTemplate(heroBlock)}
</section>
)}
{/* Main Content */}
<section className="about-content" style={{ padding: '3rem 2rem' }}>
{otherBlocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type} style={{ marginBottom: '2rem' }}>
{renderTemplate(block)}
</div>
))}
</section>
{/* Services/Features Section */}
{servicesBlock && (
<section className="about-services" style={{ padding: '3rem 2rem', background: '#f9fafb' }}>
{renderTemplate(servicesBlock)}
</section>
)}
{/* Stats Section */}
{statsBlock && (
<section className="about-stats" style={{ padding: '3rem 2rem' }}>
{renderTemplate(statsBlock)}
</section>
)}
</div>
);
}
/**
* Services Page Template
* Structure: Title → Service Cards → Details
*/
function renderServicesPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
const heroBlock = blocks.find(b => b.type === 'hero');
const servicesBlock = blocks.find(b => b.type === 'services' || b.type === 'features');
const otherBlocks = blocks.filter(b =>
b.type !== 'hero' && b.type !== 'services' && b.type !== 'features'
);
return (
<div className="page-services" data-page-slug={page.slug}>
{/* Hero/Header */}
{heroBlock ? (
<section className="services-hero">
{renderTemplate(heroBlock)}
</section>
) : (
<header style={{ marginBottom: '2rem', textAlign: 'center', padding: '2rem 0' }}>
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>
{page.title}
</h1>
</header>
)}
{/* Services Grid */}
{servicesBlock && (
<section className="services-grid" style={{ padding: '2rem 0' }}>
{renderTemplate(servicesBlock)}
</section>
)}
{/* Additional Content */}
{otherBlocks.length > 0 && (
<section className="services-content" style={{ padding: '2rem 0' }}>
{otherBlocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))}
</section>
)}
</div>
);
}
/**
* Custom Page Template
* Default rendering for custom pages - renders all blocks in order
*/
function renderCustomPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
return (
<div className="page-custom" data-page-slug={page.slug}>
<header style={{ marginBottom: '2rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
{page.title}
</h1>
</header>
<section className="custom-content" style={{ padding: '2rem 0' }}>
{blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type} style={{ marginBottom: '2rem' }}>
{renderTemplate(block)}
</div>
))}
</section>
</div>
);
}