- 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.
246 lines
6.8 KiB
TypeScript
246 lines
6.8 KiB
TypeScript
/**
|
|
* Layout Renderer
|
|
* Phase 5: Sites Renderer & Publishing
|
|
*
|
|
* Renders different layout types for sites using shared components.
|
|
*/
|
|
import React from 'react';
|
|
import type { SiteDefinition } from '../types';
|
|
import { renderTemplate } from './templateEngine';
|
|
import { renderPageByType } from './pageTypeRenderer';
|
|
import {
|
|
DefaultLayout,
|
|
MinimalLayout,
|
|
MagazineLayout,
|
|
EcommerceLayout,
|
|
PortfolioLayout,
|
|
BlogLayout,
|
|
CorporateLayout,
|
|
} from '@shared/layouts';
|
|
|
|
export type LayoutType =
|
|
| 'default'
|
|
| 'minimal'
|
|
| 'magazine'
|
|
| 'ecommerce'
|
|
| 'portfolio'
|
|
| 'blog'
|
|
| 'corporate';
|
|
|
|
/**
|
|
* Render site layout based on site definition.
|
|
*/
|
|
export function renderLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const layoutType = siteDefinition.layout as LayoutType;
|
|
|
|
switch (layoutType) {
|
|
case 'minimal':
|
|
return renderMinimalLayout(siteDefinition);
|
|
case 'magazine':
|
|
return renderMagazineLayout(siteDefinition);
|
|
case 'ecommerce':
|
|
return renderEcommerceLayout(siteDefinition);
|
|
case 'portfolio':
|
|
return renderPortfolioLayout(siteDefinition);
|
|
case 'blog':
|
|
return renderBlogLayout(siteDefinition);
|
|
case 'corporate':
|
|
return renderCorporateLayout(siteDefinition);
|
|
case 'default':
|
|
default:
|
|
return renderDefaultLayout(siteDefinition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default layout: Standard header, content, footer.
|
|
* Uses shared DefaultLayout component with fully styled modern design.
|
|
*/
|
|
function renderDefaultLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
// Find home page for hero (only show hero on home page or when showing all pages)
|
|
const homePage = siteDefinition.pages.find(p => p.slug === 'home');
|
|
const heroBlock = homePage?.blocks?.find(b => b.type === 'hero');
|
|
|
|
// Only show hero if we're on home page or showing all pages
|
|
const isHomePage = siteDefinition.pages.length === 1 && siteDefinition.pages[0]?.slug === 'home';
|
|
const showHero = isHomePage || (homePage && siteDefinition.pages.length > 1);
|
|
const hero: React.ReactNode = (showHero && heroBlock) ? (renderTemplate(heroBlock) as React.ReactNode) : undefined;
|
|
|
|
// 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) => {
|
|
// 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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, blocksToRender)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<DefaultLayout
|
|
hero={hero as any}
|
|
sections={sections}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Minimal layout: Clean, minimal design.
|
|
* Uses shared MinimalLayout component.
|
|
*/
|
|
function renderMinimalLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const mainContent = (
|
|
<>
|
|
{siteDefinition.pages
|
|
.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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, page.blocks || [])}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<MinimalLayout>
|
|
{mainContent}
|
|
</MinimalLayout>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Magazine layout: Editorial, content-focused.
|
|
* Uses shared MagazineLayout component.
|
|
*/
|
|
function renderMagazineLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const mainContent = (
|
|
<>
|
|
{siteDefinition.pages
|
|
.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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, page.blocks || [])}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<MagazineLayout
|
|
mainContent={mainContent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Ecommerce layout: Product-focused.
|
|
* Uses shared EcommerceLayout component.
|
|
*/
|
|
function renderEcommerceLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const mainContent = (
|
|
<>
|
|
{siteDefinition.pages
|
|
.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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, page.blocks || [])}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<EcommerceLayout
|
|
mainContent={mainContent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Portfolio layout: Showcase.
|
|
* Uses shared PortfolioLayout component.
|
|
*/
|
|
function renderPortfolioLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const mainContent = (
|
|
<>
|
|
{siteDefinition.pages
|
|
.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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, page.blocks || [])}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<PortfolioLayout
|
|
mainContent={mainContent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Blog layout: Content-first.
|
|
* Uses shared BlogLayout component.
|
|
*/
|
|
function renderBlogLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const mainContent = (
|
|
<>
|
|
{siteDefinition.pages
|
|
.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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, page.blocks || [])}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<BlogLayout
|
|
mainContent={mainContent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Corporate layout: Business.
|
|
* Uses shared CorporateLayout component.
|
|
*/
|
|
function renderCorporateLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
|
const mainContent = (
|
|
<>
|
|
{siteDefinition.pages
|
|
.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-wrapper" data-page-slug={page.slug}>
|
|
{renderPageByType(page, page.blocks || [])}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<CorporateLayout
|
|
mainContent={mainContent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Note: Navigation and page rendering are now handled within each layout component
|