- Gallery now shows only the 5 AI-generated product photos - Each thumb shows model name (Nano Banana / Pro) + caption - Caption overlay on main image describes the shot type - Removed original scraped images from gallery display
451 lines
19 KiB
JavaScript
451 lines
19 KiB
JavaScript
/* JustVitamin × QuikCue — Live AI Demos
|
||
Demo A renders as a real conversion-optimised PDP page */
|
||
|
||
const $ = s => document.querySelector(s);
|
||
const esc = s => { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
||
|
||
function setStep(id, cls, text) {
|
||
const el = $(id);
|
||
if (!el) return;
|
||
el.parentElement.className = `step ${cls}`;
|
||
el.innerHTML = text;
|
||
}
|
||
function setBtn(id, loading, text) {
|
||
const btn = $(id);
|
||
btn.disabled = loading;
|
||
btn.classList.toggle('loading', loading);
|
||
btn.textContent = text;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// DEMO A — Product URL → Full PDP + Real Product Images
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
let demoA_product = null;
|
||
let demoA_pack = null;
|
||
let demoA_imgs = null;
|
||
|
||
async function runDemoA() {
|
||
const url = $('#demoA-url').value.trim();
|
||
if (!url) return;
|
||
|
||
setBtn('#demoA-btn', true, 'Working...');
|
||
$('#demoA-output').classList.add('hidden');
|
||
['#a-s1','#a-s2','#a-s3'].forEach(s => setStep(s, '', 'Waiting'));
|
||
|
||
// Step 1: Scrape
|
||
setStep('#a-s1', 'active', 'Scraping product page...');
|
||
try {
|
||
const r = await fetch('/api/scrape', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({url})
|
||
});
|
||
const product = await r.json();
|
||
if (product.error) throw new Error(product.error);
|
||
demoA_product = product;
|
||
setStep('#a-s1', 'done', `✓ ${esc(product.title.substring(0,40))}...`);
|
||
} catch(e) {
|
||
setStep('#a-s1', 'error', `✗ ${esc(e.message)}`);
|
||
setBtn('#demoA-btn', false, '🔴 Generate the Whole Pack');
|
||
return;
|
||
}
|
||
|
||
// Step 2 + 3: AI copy + images in parallel
|
||
setStep('#a-s2', 'active', 'Gemini generating PDP copy...');
|
||
setStep('#a-s3', 'active', 'Generating product images from real photo...');
|
||
|
||
const packP = fetch('/api/generate-pack', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(demoA_product)
|
||
}).then(r => r.json());
|
||
|
||
const imgP = fetch('/api/generate-images', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(demoA_product)
|
||
}).then(r => r.json());
|
||
|
||
let packOk = false, imgsOk = false;
|
||
|
||
try {
|
||
demoA_pack = await packP;
|
||
if (demoA_pack.error) throw new Error(demoA_pack.error);
|
||
packOk = true;
|
||
setStep('#a-s2', 'done', `✓ PDP + assets generated (${demoA_pack._generation_time})`);
|
||
} catch(e) {
|
||
setStep('#a-s2', 'error', `✗ ${esc(e.message)}`);
|
||
}
|
||
|
||
try {
|
||
demoA_imgs = await imgP;
|
||
if (demoA_imgs.error && !demoA_imgs.original) throw new Error(demoA_imgs.error);
|
||
imgsOk = true;
|
||
setStep('#a-s3', 'done', `✓ Product images generated (${demoA_imgs._generation_time || ''})`);
|
||
} catch(e) {
|
||
setStep('#a-s3', 'error', `✗ ${esc(e.message)}`);
|
||
}
|
||
|
||
if (packOk) {
|
||
renderPDP(demoA_product, demoA_pack, demoA_imgs);
|
||
$('#demoA-output').classList.remove('hidden');
|
||
}
|
||
|
||
setBtn('#demoA-btn', false, '✓ Done — Run Again');
|
||
}
|
||
|
||
|
||
// ── Render as a real PDP page ───────────────────────────────
|
||
|
||
function renderPDP(product, pack, imgs) {
|
||
const pdp = pack.pdp || {};
|
||
const out = $('#demoA-pdp');
|
||
if (!out) return;
|
||
|
||
// Build gallery images
|
||
const gallery = buildGallery(product, imgs);
|
||
const bullets = (pdp.benefit_bullets || []).map(b =>
|
||
`<div class="pdp-bullet">
|
||
<span class="pdp-bullet-icon">${esc(b.icon||'✓')}</span>
|
||
<div><strong>${esc(b.headline)}</strong><br>
|
||
<span class="pdp-bullet-detail">${esc(b.detail)}</span>
|
||
${b.proof ? `<span class="pdp-bullet-proof">${esc(b.proof)}</span>` : ''}
|
||
</div>
|
||
</div>`
|
||
).join('');
|
||
|
||
const trustBar = (pdp.trust_signals || []).map(t =>
|
||
`<div class="pdp-trust-item"><span class="pdp-trust-icon">${esc(t.icon)}</span><span>${esc(t.text)}</span></div>`
|
||
).join('');
|
||
|
||
const whyParas = (pdp.why_paragraphs || []).map(p => `<p>${esc(p)}</p>`).join('');
|
||
|
||
const statsBar = (pdp.stats_bar || []).map(s =>
|
||
`<div class="pdp-stat"><span class="pdp-stat-num">${esc(s.number)}</span><span class="pdp-stat-label">${esc(s.label)}</span></div>`
|
||
).join('');
|
||
|
||
const faq = (pdp.faq || []).map((f,i) =>
|
||
`<details class="pdp-faq-item" ${i===0?'open':''}>
|
||
<summary>${esc(f.q)}</summary>
|
||
<p>${esc(f.a)}</p>
|
||
</details>`
|
||
).join('');
|
||
|
||
const priceInfo = pdp.price_display || {};
|
||
const review = pdp.review_quote || {};
|
||
|
||
// Ad hooks section
|
||
const hooks = (pack.ad_hooks || []).map(h =>
|
||
`<div class="pdp-hook">
|
||
<div class="pdp-hook-text">"${esc(h.hook || h)}"</div>
|
||
${h.angle ? `<span class="pdp-hook-meta">${esc(h.angle)} · ${esc(h.platform||'')}</span>` : ''}
|
||
</div>`
|
||
).join('');
|
||
|
||
// Email subjects
|
||
const emails = (pack.email_subjects || []).map(e =>
|
||
`<div class="pdp-email">
|
||
<span class="pdp-email-flow">${esc(e.flow||'')}</span>
|
||
<strong>${esc(e.subject)}</strong>
|
||
<span class="pdp-email-preview">${esc(e.preview)}</span>
|
||
</div>`
|
||
).join('');
|
||
|
||
// Meta SEO
|
||
const meta = pack.meta_seo || {};
|
||
|
||
out.innerHTML = `
|
||
<!-- ═══ PDP LAYOUT ═══ -->
|
||
<div class="pdp-page">
|
||
|
||
<!-- ABOVE THE FOLD -->
|
||
<div class="pdp-atf">
|
||
<div class="pdp-gallery">${gallery}</div>
|
||
<div class="pdp-info">
|
||
<div class="pdp-breadcrumb">${esc(product.category)} › ${esc(product.title.split(' ').slice(0,4).join(' '))}...</div>
|
||
<h2 class="pdp-title">${esc(pdp.hero_headline || product.title)}</h2>
|
||
<p class="pdp-subhead">${esc(pdp.hero_subhead || product.subtitle)}</p>
|
||
|
||
<div class="pdp-value-props">
|
||
${(pdp.value_props||[]).map(v=>`<span class="pdp-vp">✓ ${esc(v)}</span>`).join('')}
|
||
</div>
|
||
|
||
<div class="pdp-price-block">
|
||
<span class="pdp-price-main">${esc(priceInfo.main_price || product.price)}</span>
|
||
<span class="pdp-price-qty">${esc(product.quantity)}</span>
|
||
<div class="pdp-price-daily">${esc(priceInfo.price_per_day || '')}</div>
|
||
<div class="pdp-price-compare">${esc(priceInfo.comparison || '')}</div>
|
||
</div>
|
||
|
||
<div class="pdp-trust-bar">${trustBar}</div>
|
||
|
||
<button class="pdp-cta-primary">${esc(pdp.cta_primary || 'Add to Basket')}</button>
|
||
<button class="pdp-cta-secondary">${esc(pdp.cta_secondary || 'Subscribe & Save 15%')}</button>
|
||
|
||
${pdp.urgency_note ? `<div class="pdp-urgency">⚡ ${esc(pdp.urgency_note)}</div>` : ''}
|
||
<div class="pdp-usage">${esc(pdp.usage_instructions || '')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BENEFIT BULLETS -->
|
||
<div class="pdp-section">
|
||
<h3>Key Benefits</h3>
|
||
<div class="pdp-bullets">${bullets}</div>
|
||
</div>
|
||
|
||
<!-- STATS BAR -->
|
||
<div class="pdp-stats-bar">${statsBar}</div>
|
||
|
||
<!-- WHY THIS FORMULA -->
|
||
<div class="pdp-section pdp-why">
|
||
<h3>${esc(pdp.why_section_title || 'Why This Formula')}</h3>
|
||
${whyParas}
|
||
</div>
|
||
|
||
<!-- SOCIAL PROOF -->
|
||
<div class="pdp-review-section">
|
||
<div class="pdp-review-card">
|
||
<div class="pdp-review-stars">${'★'.repeat(review.stars||5)}</div>
|
||
<p class="pdp-review-text">"${esc(review.text || '')}"</p>
|
||
<span class="pdp-review-author">— ${esc(review.author || 'Verified Buyer')}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FAQ -->
|
||
<div class="pdp-section">
|
||
<h3>Frequently Asked Questions</h3>
|
||
<div class="pdp-faq">${faq}</div>
|
||
</div>
|
||
|
||
<!-- ═══ ADDITIONAL ASSETS ═══ -->
|
||
<div class="pdp-divider">
|
||
<span>📦 Additional Generated Assets</span>
|
||
</div>
|
||
|
||
<!-- META SEO -->
|
||
${meta.title ? `
|
||
<div class="pdp-asset-section">
|
||
<h4>🔍 Meta SEO</h4>
|
||
<div class="pdp-seo">
|
||
<div class="pdp-seo-title">${esc(meta.title)}</div>
|
||
<div class="pdp-seo-url">justvitamins.co.uk › ${esc(meta.primary_keyword || '')}</div>
|
||
<div class="pdp-seo-desc">${esc(meta.description)}</div>
|
||
</div>
|
||
</div>` : ''}
|
||
|
||
<!-- AD HOOKS -->
|
||
${hooks ? `
|
||
<div class="pdp-asset-section">
|
||
<h4>📢 Ad Hooks</h4>
|
||
<div class="pdp-hooks-grid">${hooks}</div>
|
||
</div>` : ''}
|
||
|
||
<!-- EMAIL SUBJECTS -->
|
||
${emails ? `
|
||
<div class="pdp-asset-section">
|
||
<h4>📧 Email Sequences</h4>
|
||
<div class="pdp-emails">${emails}</div>
|
||
</div>` : ''}
|
||
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
function buildGallery(product, imgs) {
|
||
const items = [];
|
||
// AI-generated images only
|
||
if (imgs) {
|
||
const aiSlots = [
|
||
{key:'hero', label:'Clean Studio Shot', caption:'Studio lighting, white background'},
|
||
{key:'lifestyle', label:'Lifestyle Shot', caption:'Morning kitchen scene'},
|
||
{key:'scale', label:'Scale Reference', caption:'Real-world size context'},
|
||
{key:'ingredients', label:'Detail Close-up', caption:'Ingredients & quality'},
|
||
{key:'banner', label:'Hero Banner', caption:'Wide format for landing pages', wide: true},
|
||
];
|
||
aiSlots.forEach(slot => {
|
||
const d = imgs[slot.key];
|
||
if (d && d.filename) {
|
||
items.push({src: `/generated/${d.filename}`, label: slot.label,
|
||
caption: d.caption || slot.caption, model: d.model || '', wide: slot.wide});
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!items.length) return '<div class="pdp-gallery-empty">Generating product images...</div>';
|
||
|
||
const mainImg = items[0];
|
||
const thumbs = items.map((item, i) =>
|
||
`<div class="pdp-thumb ${i===0?'active':''} ${item.wide?'wide':''}" onclick="switchGallery(${i}, this)" data-src="${esc(item.src)}" data-caption="${esc(item.caption)}" data-model="${esc(item.model)}">
|
||
<img src="${esc(item.src)}" alt="${esc(item.label)}" loading="lazy">
|
||
<span class="pdp-thumb-label ai">${esc(item.label)}</span>
|
||
</div>`
|
||
).join('');
|
||
|
||
return `
|
||
<div class="pdp-gallery-main" id="pdpGalleryMain">
|
||
<img src="${esc(mainImg.src)}" alt="${esc(mainImg.label)}" id="pdpMainImg">
|
||
<span class="pdp-gallery-badge">🤖 AI Generated — ${esc(mainImg.model)}</span>
|
||
<span class="pdp-gallery-caption" id="pdpCaption">${esc(mainImg.caption)}</span>
|
||
</div>
|
||
<div class="pdp-thumbs">${thumbs}</div>
|
||
`;
|
||
}
|
||
|
||
window.switchGallery = function(idx, el) {
|
||
const src = el.dataset.src;
|
||
const mainImg = $('#pdpMainImg');
|
||
if (mainImg) mainImg.src = src;
|
||
document.querySelectorAll('.pdp-thumb').forEach(t => t.classList.remove('active'));
|
||
el.classList.add('active');
|
||
const badge = $('.pdp-gallery-badge');
|
||
if (badge) badge.textContent = `🤖 AI Generated — ${el.dataset.model || ''}`;
|
||
const caption = $('#pdpCaption');
|
||
if (caption) caption.textContent = el.dataset.caption || '';
|
||
};
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// DEMO B — Competitor X-Ray (unchanged logic, same render)
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
async function runDemoB() {
|
||
const url = $('#demoB-url').value.trim();
|
||
if (!url) return;
|
||
setBtn('#demoB-btn', true, 'Scanning...');
|
||
$('#demoB-output').classList.add('hidden');
|
||
setStep('#b-s1', 'active', 'Scraping competitor...');
|
||
setStep('#b-s2', '', 'Waiting');
|
||
|
||
try {
|
||
const r = await fetch('/api/competitor-xray', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({url})
|
||
});
|
||
const data = await r.json();
|
||
if (data.error) throw new Error(data.error);
|
||
setStep('#b-s1', 'done', `✓ ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
||
setStep('#b-s2', 'done', `✓ Analysis (${data._generation_time})`);
|
||
renderXray(data);
|
||
$('#demoB-output').classList.remove('hidden');
|
||
setBtn('#demoB-btn', false, '✓ Done — Try Another');
|
||
} catch(e) {
|
||
setStep('#b-s1', 'error', `✗ ${esc(e.message)}`);
|
||
setBtn('#demoB-btn', false, '🔍 X-Ray This Competitor');
|
||
}
|
||
}
|
||
|
||
function renderXray(data) {
|
||
const tactics = (data.top_5_tactics||[]).map(t =>
|
||
`<li><strong>${esc(t.tactic)}</strong> — ${esc(t.explanation)}</li>`).join('');
|
||
$('#demoB-left').innerHTML = `
|
||
<span class="split-label bad">❌ ${esc(data.competitor_name || 'Competitor')}</span>
|
||
<div class="xray-item"><div class="xray-label">What they're really selling</div>
|
||
<div class="xray-val"><strong>${esc(data.what_theyre_selling)}</strong></div></div>
|
||
<div class="xray-item"><div class="xray-label">Top 5 Persuasion Tactics</div>
|
||
<ol class="xray-tactics">${tactics}</ol></div>
|
||
<div class="xray-item"><div class="xray-label">Weakest Claim / Gap</div>
|
||
<div class="xray-val gap">⚠️ ${esc(data.weakest_claim)}</div></div>`;
|
||
|
||
const hero = data.jv_hero_section || {};
|
||
const diffs = (data.differentiators||[]).map(d =>
|
||
`<li><span class="icon">🎯</span><span class="txt"><strong>${esc(d.point)}</strong> — ${esc(d.proof_idea)}</span></li>`).join('');
|
||
const donts = (data.do_not_say||[]).map(d => `<li>${esc(d)}</li>`).join('');
|
||
$('#demoB-right').innerHTML = `
|
||
<span class="split-label good">✓ Just Vitamins — Upgraded</span>
|
||
<div class="improved-hero"><h4>${esc(hero.headline)}</h4><p>${esc(hero.body)}</p>
|
||
<p style="margin-top:.5rem;color:var(--accent);font-weight:600">${esc(hero.value_prop)}</p></div>
|
||
<div class="xray-item"><div class="xray-label">3 Differentiators</div><ul class="diff-list">${diffs}</ul></div>
|
||
<div class="compliance"><h5>⚠️ Do Not Say</h5><ul>${donts}</ul></div>`;
|
||
}
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// DEMO C — PDP Surgeon
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
let demoC_product = null, demoC_cache = {};
|
||
|
||
async function runDemoC() {
|
||
const url = $('#demoC-url').value.trim();
|
||
if (!url) return;
|
||
setBtn('#demoC-btn', true, 'Working...');
|
||
$('#demoC-output').classList.add('hidden');
|
||
demoC_cache = {};
|
||
setStep('#c-s1', 'active', 'Scraping product...');
|
||
setStep('#c-s2', '', 'Waiting');
|
||
try {
|
||
const r = await fetch('/api/scrape', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
|
||
const product = await r.json();
|
||
if (product.error) throw new Error(product.error);
|
||
demoC_product = product;
|
||
setStep('#c-s1', 'done', `✓ ${esc(product.title.substring(0,35))}...`);
|
||
} catch(e) {
|
||
setStep('#c-s1', 'error', `✗ ${esc(e.message)}`);
|
||
setBtn('#demoC-btn', false, '🎨 Scrape & Rewrite');
|
||
return;
|
||
}
|
||
const active = document.querySelector('#demoC-toggles .toggle.active');
|
||
await rewriteStyle(active?.dataset.style || 'balanced');
|
||
setBtn('#demoC-btn', false, '✓ Done — Change URL');
|
||
}
|
||
|
||
async function switchDemoC(style, btn) {
|
||
document.querySelectorAll('#demoC-toggles .toggle').forEach(t => t.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
if (!demoC_product) return;
|
||
await rewriteStyle(style);
|
||
}
|
||
|
||
async function rewriteStyle(style) {
|
||
if (demoC_cache[style]) { renderSurgeon(demoC_product, demoC_cache[style]); return; }
|
||
setStep('#c-s2', 'active', `Gemini rewriting as ${style}...`);
|
||
try {
|
||
const r = await fetch('/api/pdp-surgeon', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({product:demoC_product,style})});
|
||
const result = await r.json();
|
||
if (result.error) throw new Error(result.error);
|
||
demoC_cache[style] = result;
|
||
setStep('#c-s2', 'done', `✓ ${style} (${result._generation_time})`);
|
||
renderSurgeon(demoC_product, result);
|
||
$('#demoC-output').classList.remove('hidden');
|
||
} catch(e) { setStep('#c-s2', 'error', `✗ ${esc(e.message)}`); }
|
||
}
|
||
|
||
function renderSurgeon(product, result) {
|
||
const bullets = (product.benefits||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
||
$('#demoC-left').innerHTML = `
|
||
<span class="split-label bad">✕ Current PDP</span>
|
||
${product.images?.[0] ? `<img src="${esc(product.images[0])}" style="width:100%;border-radius:8px;margin-bottom:.75rem" alt="Current">` : ''}
|
||
<h4 style="font-size:1rem;margin-bottom:.5rem">${esc(product.title)}</h4>
|
||
<p style="font-size:.84rem;color:var(--muted);margin-bottom:.5rem">${esc(product.subtitle)}</p>
|
||
<p style="font-size:1.2rem;font-weight:800;color:var(--accent);margin-bottom:.5rem">${esc(product.price)} <span style="font-size:.78rem;color:var(--muted)">${esc(product.quantity)}</span></p>
|
||
<ul style="list-style:none">${bullets.replace(/<li>/g,'<li style="padding:.2rem 0;font-size:.82rem;color:var(--text2)">')}</ul>`;
|
||
|
||
const rBullets = (result.bullets||[]).map(b =>
|
||
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`).join('');
|
||
$('#demoC-right').innerHTML = `
|
||
<span class="split-label good">✓ ${esc(result.style||'balanced').toUpperCase()}</span>
|
||
<h4 style="font-size:1rem;margin-bottom:.25rem">${esc(result.title)}</h4>
|
||
<span class="ann">↑ SEO title</span>
|
||
<p style="font-size:.84rem;color:var(--accent2);margin:.4rem 0">${esc(result.subtitle)}</p>
|
||
<div class="highlight" style="font-weight:600">${esc(result.hero_copy)}</div>
|
||
<span class="ann">↑ ${esc(result.hero_annotation)}</span>
|
||
<div style="margin-top:.6rem">${rBullets}</div>
|
||
<div style="margin-top:.6rem;padding:.5rem .7rem;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);border-radius:8px;font-size:.82rem;color:var(--gold)">⭐ ${esc(result.social_proof)}</div>
|
||
<span class="ann">↑ ${esc(result.social_proof_annotation)}</span>
|
||
<div style="margin-top:.6rem;font-size:1rem;font-weight:800;color:var(--accent)">${esc(result.price_reframe)}</div>
|
||
<span class="ann">↑ ${esc(result.price_annotation)}</span>
|
||
<p style="margin-top:.5rem;font-size:.82rem;color:var(--text2);font-style:italic">${esc(result.usage_instruction)}</p>
|
||
<span class="ann">↑ ${esc(result.usage_annotation)}</span>
|
||
<div style="margin-top:.75rem;text-align:center">
|
||
<button style="background:var(--accent);color:#060a0f;border:none;padding:.65rem 1.8rem;border-radius:8px;font-size:.88rem;font-weight:700;cursor:pointer">${esc(result.cta_text||'Add to Basket')}</button>
|
||
</div>
|
||
${result.cta_annotation ? `<span class="ann" style="display:block;text-align:center;margin-top:.3rem">↑ ${esc(result.cta_annotation)}</span>` : ''}`;
|
||
}
|
||
|
||
// Smooth scroll
|
||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||
a.addEventListener('click', e => {
|
||
const t = document.querySelector(a.getAttribute('href'));
|
||
if (t) { e.preventDefault(); t.scrollIntoView({behavior:'smooth'}); }
|
||
});
|
||
});
|