← back to Professional Directory
agents/web-agent/app/mockups/page.tsx
98 lines
// /mockups — admin gallery of generated per-org website mockups.
// Pulls _manifest.json from data/mockups/, renders a grid of iframe thumbnails.
import fs from 'node:fs';
import path from 'node:path';
const PD_API = process.env.PD_API_BASE || 'http://127.0.0.1:9874';
const MOCKUPS_DIR = path.resolve(process.cwd(), '../../data/mockups');
type ManifestEntry = {
id: number;
name: string;
status: 'ok' | 'cached' | 'fail';
ms?: number;
kb?: number;
template?: string;
};
async function getManifest(): Promise<ManifestEntry[]> {
try {
const raw = fs.readFileSync(path.join(MOCKUPS_DIR, '_manifest.json'), 'utf8');
const j = JSON.parse(raw);
return j.results || [];
} catch { return []; }
}
async function getOrgs(ids: number[]) {
if (!ids.length) return new Map<number, any>();
// Hit pd-api for org details (fast — small set)
const out = new Map<number, any>();
for (const id of ids.slice(0, 60)) {
try {
const r = await fetch(`${PD_API}/organizations/${id}`, { next: { revalidate: 600 } });
if (r.ok) out.set(id, await r.json());
} catch {}
}
return out;
}
export default async function MockupsPage() {
const manifest = await getManifest();
const ids = manifest.filter(r => r.status === 'ok' || r.status === 'cached').map(r => r.id);
const orgs = await getOrgs(ids);
return (
<section className="section" style={{ maxWidth: 'none' }}>
<div className="eyebrow">Admin · Generated mockups</div>
<h2>Top-20 bespoke website mockups</h2>
<p style={{ color: 'var(--ink-soft)', maxWidth: 720 }}>
Per-org single-page mockups generated by gemma3:12b on Mac Studio 2 using template-rotation
(8 top-rated healthcare practice sites) + healthcare-modern aesthetic + SEO-tuned JSON-LD.
Each mockup includes a floating AI-chat widget for visitors to ask about hours, services, location.
</p>
{manifest.length === 0 ? (
<div className="card" style={{ marginTop: 32, padding: 32, textAlign: 'center' }}>
<p>No mockups yet. Run <code>node scripts/generate-mockups.js</code>.</p>
</div>
) : (
<div className="card-grid" style={{ marginTop: 32, gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))' }}>
{manifest.filter(r => r.status === 'ok' || r.status === 'cached').map((r) => {
const o = orgs.get(r.id);
const url = `${PD_API}/mockups/${r.id}.html`;
return (
<a key={r.id} className="card" href={url} target="_blank" rel="noopener" style={{ padding: 0, overflow: 'hidden' }}>
<iframe
src={url}
style={{ width: '100%', aspectRatio: '5/4', border: 0, background: '#fbf7f0', pointerEvents: 'none' }}
loading="lazy"
scrolling="no"
title={`Mockup for ${r.name}`}
/>
<div style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
<span className="badge">{r.template || 'tpl'}</span>
<span className="name">{titleCase(r.name)}</span>
<span className="meta">
{o ? `${titleCase(o.city || '')} · ${o.type?.replace(/_/g, ' ')}` : `org #${r.id}`}
{r.kb ? ` · ${r.kb}KB` : ''}{r.ms ? ` · ${(r.ms / 1000).toFixed(1)}s gen` : ''}
</span>
</div>
</a>
);
})}
</div>
)}
{manifest.some(r => r.status === 'fail') && (
<div style={{ marginTop: 32, padding: 16, background: '#f5e6d3', border: '1px solid var(--gold)', borderRadius: 8 }}>
<strong>Failed:</strong>{' '}
{manifest.filter(r => r.status === 'fail').map(r => r.name).join(', ')}
</div>
)}
</section>
);
}
function titleCase(s: string) { return String(s || '').toLowerCase().replace(/\b\w/g, c => c.toUpperCase()); }