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:
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
374
sites/src/utils/pageTypeRenderer.tsx
Normal file
374
sites/src/utils/pageTypeRenderer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user