Files
justvitamin/static/js/app.js
Omair Saleh 7e58ab1970 v4: Real product image generation + conversion PDP output
Image Generation:
- Downloads actual product images from justvitamins.co.uk
- Sends real photo as reference to Gemini (image-to-image)
- Generates 5 ecommerce-grade variations maintaining product consistency:
  Hero (clean studio), Lifestyle (kitchen scene), Scale (hand reference),
  Detail (ingredients close-up), Banner (wide hero)
- Uses Nano Banana Pro for hero/lifestyle/banner, Nano Banana for fast shots

PDP Output:
- Demo A now renders as a real ecommerce product detail page
- Gallery: original + AI-generated images with clickable thumbnails
- Above the fold: H1, value props, price block, trust bar, CTAs
- Key Benefits: Feature → Benefit → Proof format, 5 icon cards
- Stats bar, Why This Formula, 5★ review, FAQ accordion
- Meta SEO (Google preview), Ad Hooks (5 platform-targeted), Email sequences

Prompts:
- Conversion-optimised based on Cialdini/Kahneman principles
- EFSA health claim compliance baked into every prompt
- Feature → Benefit → Proof bullet structure
- Price anchoring, social proof, urgency psychology
2026-03-02 20:41:30 +08:00

455 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 = [];
// Original product images first
(product.images || []).forEach((src, i) => {
items.push({src: src, label: i === 0 ? 'Original — Main' : `Original — ${i+1}`, isOriginal: true});
});
// AI-generated images
if (imgs) {
const aiSlots = [
{key:'hero', label:'AI — Clean Studio'},
{key:'lifestyle', label:'AI — Lifestyle'},
{key:'scale', label:'AI — Scale'},
{key:'ingredients', label:'AI — Detail'},
{key:'banner', label:'AI — Banner', 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 || '', model: d.model || '', wide: slot.wide});
}
});
}
if (!items.length) return '<div class="pdp-gallery-empty">No images available</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)}">
<img src="${esc(item.src)}" alt="${esc(item.label)}" loading="lazy">
<span class="pdp-thumb-label ${item.isOriginal?'orig':'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">${mainImg.isOriginal ? '📷 Original' : '🤖 AI Generated'}</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) {
const label = el.querySelector('.pdp-thumb-label');
badge.textContent = label?.classList.contains('orig') ? '📷 Original' : '🤖 AI Generated';
}
};
// ═══════════════════════════════════════════════════════════════
// 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'}); }
});
});