← 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()); }