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
This commit is contained in:
466
static/js/app.js
466
static/js/app.js
@@ -1,4 +1,5 @@
|
||||
/* JustVitamin × QuikCue — Live AI Demos */
|
||||
/* 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; };
|
||||
@@ -9,7 +10,6 @@ function setStep(id, cls, text) {
|
||||
el.parentElement.className = `step ${cls}`;
|
||||
el.innerHTML = text;
|
||||
}
|
||||
|
||||
function setBtn(id, loading, text) {
|
||||
const btn = $(id);
|
||||
btn.disabled = loading;
|
||||
@@ -18,10 +18,12 @@ function setBtn(id, loading, text) {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DEMO A — One Product → 12 Assets + Images
|
||||
// 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();
|
||||
@@ -32,7 +34,7 @@ async function runDemoA() {
|
||||
['#a-s1','#a-s2','#a-s3'].forEach(s => setStep(s, '', 'Waiting'));
|
||||
|
||||
// Step 1: Scrape
|
||||
setStep('#a-s1', 'active', '<span class="spinner"></span> Scraping...');
|
||||
setStep('#a-s1', 'active', 'Scraping product page...');
|
||||
try {
|
||||
const r = await fetch('/api/scrape', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
@@ -41,16 +43,16 @@ async function runDemoA() {
|
||||
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,35))}...`);
|
||||
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: AI pack + Step 3: Images (parallel)
|
||||
setStep('#a-s2', 'active', '<span class="spinner"></span> Gemini generating 12 assets...');
|
||||
setStep('#a-s3', 'active', '<span class="spinner"></span> Nano Banana generating images...');
|
||||
// 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'},
|
||||
@@ -62,164 +64,259 @@ async function runDemoA() {
|
||||
body: JSON.stringify(demoA_product)
|
||||
}).then(r => r.json());
|
||||
|
||||
// Handle pack
|
||||
let packOk = false, imgsOk = false;
|
||||
|
||||
try {
|
||||
const pack = await packP;
|
||||
if (pack.error) throw new Error(pack.error);
|
||||
setStep('#a-s2', 'done', `✓ 12 assets generated (${pack._generation_time})`);
|
||||
renderAssetPack(pack);
|
||||
$('#demoA-output').classList.remove('hidden');
|
||||
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)}`);
|
||||
}
|
||||
|
||||
// Handle images
|
||||
try {
|
||||
const imgs = await imgP;
|
||||
if (imgs.error) throw new Error(imgs.error);
|
||||
setStep('#a-s3', 'done', `✓ Images generated (${imgs._generation_time})`);
|
||||
renderImages(imgs);
|
||||
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');
|
||||
}
|
||||
|
||||
function renderAssetPack(pack) {
|
||||
const meta = $('#demoA-meta');
|
||||
meta.innerHTML = `
|
||||
<span class="chip">🧠 <strong>Model:</strong> Gemini 2.5 Flash</span>
|
||||
<span class="chip">⏱ <strong>Time:</strong> ${esc(pack._generation_time)}</span>
|
||||
<span class="chip">📦 <strong>Product:</strong> ${esc(pack._product_title)}</span>
|
||||
`;
|
||||
|
||||
const grid = $('#demoA-assets');
|
||||
let cards = '';
|
||||
let n = 0;
|
||||
// ── Render as a real PDP page ───────────────────────────────
|
||||
|
||||
// Hero angles
|
||||
(pack.hero_angles || []).forEach((a, i) => {
|
||||
n++;
|
||||
cards += assetCard('hero', `Hero Angle`, n, `<strong>"${esc(a.headline)}"</strong><ul><li>Target: ${esc(a.target_desire)}</li><li>Best for: ${esc(a.best_for)}</li></ul>`, i*80);
|
||||
});
|
||||
function renderPDP(product, pack, imgs) {
|
||||
const pdp = pack.pdp || {};
|
||||
const out = $('#demoA-pdp');
|
||||
if (!out) return;
|
||||
|
||||
// PDP Copy
|
||||
if (pack.pdp_copy) {
|
||||
n++;
|
||||
const bullets = (pack.pdp_copy.bullets||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
||||
cards += assetCard('pdp', 'PDP Copy', n, `<strong>${esc(pack.pdp_copy.headline)}</strong><ul>${bullets}</ul>`, n*80);
|
||||
n++;
|
||||
const faqs = (pack.pdp_copy.faq||[]).map(f => `<strong>Q: ${esc(f.q)}</strong><br>A: ${esc(f.a)}<br><br>`).join('');
|
||||
cards += assetCard('pdp', 'FAQ Block', n, faqs, n*80);
|
||||
}
|
||||
// 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('');
|
||||
|
||||
// Ad hooks
|
||||
if (pack.ad_hooks) {
|
||||
n++;
|
||||
const hooks = pack.ad_hooks.map(h => `<li><strong>${esc(h)}</strong></li>`).join('');
|
||||
cards += assetCard('ad', '5 Ad Hooks', n, `<ul>${hooks}</ul>`, n*80);
|
||||
}
|
||||
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
|
||||
if (pack.email_subjects) {
|
||||
n++;
|
||||
const emails = pack.email_subjects.map(e => `<strong>${esc(e.subject)}</strong><br><em style="color:var(--muted)">${esc(e.preview)}</em><br><br>`).join('');
|
||||
cards += assetCard('email', 'Email Subjects', n, emails, n*80);
|
||||
}
|
||||
|
||||
// TikTok
|
||||
if (pack.tiktok_script) {
|
||||
n++;
|
||||
const t = pack.tiktok_script;
|
||||
cards += assetCard('video', 'TikTok Script', n, `
|
||||
<strong>${esc(t.title)}</strong><br><br>
|
||||
<em>[0-3s]</em> ${esc(t.hook_0_3s)}<br>
|
||||
<em>[3-12s]</em> ${esc(t.body_3_12s)}<br>
|
||||
<em>[12-15s]</em> ${esc(t.cta_12_15s)}<br><br>
|
||||
<span class="ann">${esc(t.why_it_works)}</span>
|
||||
`, n*80);
|
||||
}
|
||||
|
||||
// Blog
|
||||
if (pack.blog_outline) {
|
||||
n++;
|
||||
const b = pack.blog_outline;
|
||||
const secs = (b.sections||[]).map(s => `<li>${esc(s)}</li>`).join('');
|
||||
cards += assetCard('blog', 'Blog Outline', n, `
|
||||
<strong>${esc(b.title)}</strong><ul>${secs}</ul>
|
||||
<span class="ann">SEO: "${esc(b.seo_keyword)}" — ${esc(b.monthly_searches)} mo/searches</span>
|
||||
`, n*80);
|
||||
}
|
||||
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
|
||||
if (pack.meta_seo) {
|
||||
n++;
|
||||
const m = pack.meta_seo;
|
||||
cards += assetCard('seo', 'Meta SEO', n, `
|
||||
<strong>Title:</strong> ${esc(m.title)}<br><br>
|
||||
<strong>Description:</strong> ${esc(m.description)}<br><br>
|
||||
<span class="ann">${m.title_chars || '?'} chars title / ${m.desc_chars || '?'} chars desc</span>
|
||||
`, n*80);
|
||||
}
|
||||
const meta = pack.meta_seo || {};
|
||||
|
||||
// Alt text
|
||||
if (pack.alt_text) {
|
||||
n++;
|
||||
const alts = pack.alt_text.map(a => `<strong>${esc(a.image_type)}:</strong><br>Alt: ${esc(a.alt)}<br>File: <code style="color:var(--accent);font-size:.78rem">${esc(a.filename)}</code><br><br>`).join('');
|
||||
cards += assetCard('a11y', 'Alt Text + Filenames', n, alts, n*80);
|
||||
}
|
||||
out.innerHTML = `
|
||||
<!-- ═══ PDP LAYOUT ═══ -->
|
||||
<div class="pdp-page">
|
||||
|
||||
// A/B Variants
|
||||
if (pack.ab_variants) {
|
||||
n++;
|
||||
const vars = pack.ab_variants.map(v => `<strong>${esc(v.label)}:</strong> ${esc(v.copy)}<br><br>`).join('');
|
||||
cards += assetCard('ad', 'A/B Variants', n, vars + '<span class="ann">Test all — let data pick the winner</span>', n*80);
|
||||
}
|
||||
<!-- 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>
|
||||
|
||||
grid.innerHTML = cards;
|
||||
<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 assetCard(type, label, num, content, delay) {
|
||||
return `<div class="asset-card" style="animation-delay:${delay}ms">
|
||||
<div class="asset-card-head"><span class="asset-type ${type}">${label}</span><span class="asset-num">#${num}</span></div>
|
||||
<div class="asset-body">${content}</div></div>`;
|
||||
}
|
||||
|
||||
function renderImages(imgs) {
|
||||
const grid = $('#demoA-img-grid');
|
||||
const section = $('#demoA-images');
|
||||
let html = '';
|
||||
const styles = [
|
||||
{key:'hero', label:'Hero Banner', desc:'Nano Banana Pro', wide:true},
|
||||
{key:'lifestyle', label:'Lifestyle Shot', desc:'Nano Banana'},
|
||||
{key:'benefits', label:'Benefits Visual', desc:'Nano Banana Pro'},
|
||||
];
|
||||
styles.forEach(s => {
|
||||
const data = imgs[s.key];
|
||||
if (data && data.filename) {
|
||||
html += `<div class="img-card ${s.wide?'wide':''}">
|
||||
<img src="/generated/${data.filename}" alt="${s.label}" loading="lazy">
|
||||
<div class="caption"><strong>${s.label}</strong> — ${s.desc} · ${data.model||''}</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});
|
||||
});
|
||||
if (html) {
|
||||
grid.innerHTML = html;
|
||||
section.classList.remove('hidden');
|
||||
// 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
|
||||
// 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', '<span class="spinner"></span> Scraping competitor...');
|
||||
setStep('#b-s1', 'active', 'Scraping competitor...');
|
||||
setStep('#b-s2', '', 'Waiting');
|
||||
|
||||
try {
|
||||
@@ -229,10 +326,8 @@ async function runDemoB() {
|
||||
});
|
||||
const data = await r.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
setStep('#b-s1', 'done', `✓ Scraped: ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
||||
setStep('#b-s2', 'done', `✓ Analysis complete (${data._generation_time})`);
|
||||
|
||||
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');
|
||||
@@ -243,11 +338,8 @@ async function runDemoB() {
|
||||
}
|
||||
|
||||
function renderXray(data) {
|
||||
// Left — competitor analysis
|
||||
const tactics = (data.top_5_tactics||[]).map(t =>
|
||||
`<li><strong>${esc(t.tactic)}</strong> — ${esc(t.explanation)}</li>`
|
||||
).join('');
|
||||
|
||||
`<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>
|
||||
@@ -255,52 +347,37 @@ function renderXray(data) {
|
||||
<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>
|
||||
`;
|
||||
<div class="xray-val gap">⚠️ ${esc(data.weakest_claim)}</div></div>`;
|
||||
|
||||
// Right — JV improved
|
||||
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('');
|
||||
`<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 + Proof Ideas</div>
|
||||
<ul class="diff-list">${diffs}</ul></div>
|
||||
<div class="compliance"><h5>⚠️ Do Not Say — Compliance</h5><ul>${donts}</ul></div>
|
||||
`;
|
||||
<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;
|
||||
let demoC_cache = {};
|
||||
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', '<span class="spinner"></span> Scraping product...');
|
||||
setStep('#c-s1', 'active', 'Scraping product...');
|
||||
setStep('#c-s2', '', 'Waiting');
|
||||
|
||||
// Step 1: Scrape
|
||||
try {
|
||||
const r = await fetch('/api/scrape', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({url})
|
||||
});
|
||||
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;
|
||||
@@ -310,12 +387,8 @@ async function runDemoC() {
|
||||
setBtn('#demoC-btn', false, '🎨 Scrape & Rewrite');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: AI rewrite (default style)
|
||||
const active = document.querySelector('#demoC-toggles .toggle.active');
|
||||
const style = active?.dataset.style || 'balanced';
|
||||
await rewriteStyle(style);
|
||||
|
||||
await rewriteStyle(active?.dataset.style || 'balanced');
|
||||
setBtn('#demoC-btn', false, '✓ Done — Change URL');
|
||||
}
|
||||
|
||||
@@ -327,73 +400,52 @@ async function switchDemoC(style, btn) {
|
||||
}
|
||||
|
||||
async function rewriteStyle(style) {
|
||||
if (demoC_cache[style]) {
|
||||
renderSurgeon(demoC_product, demoC_cache[style]);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep('#c-s2', 'active', `<span class="spinner"></span> Gemini rewriting as ${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 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} rewrite (${result._generation_time})`);
|
||||
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)}`);
|
||||
}
|
||||
} catch(e) { setStep('#c-s2', 'error', `✗ ${esc(e.message)}`); }
|
||||
}
|
||||
|
||||
function renderSurgeon(product, result) {
|
||||
// Left — current
|
||||
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:.75rem">${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);font-weight:400">${esc(product.quantity)}</span></p>
|
||||
<ul style="list-style:none;margin-bottom:1rem">${bullets.replace(/<li>/g, '<li style="padding:.3rem 0;font-size:.84rem;color:var(--text2)">')}</ul>
|
||||
<p style="font-size:.84rem;color:var(--muted);line-height:1.6">${esc((product.description||'').substring(0,400))}...</p>
|
||||
`;
|
||||
<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>`;
|
||||
|
||||
// Right — rewritten
|
||||
const rBullets = (result.bullets||[]).map(b =>
|
||||
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`
|
||||
).join('');
|
||||
|
||||
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`).join('');
|
||||
$('#demoC-right').innerHTML = `
|
||||
<span class="split-label good">✓ AI-Rewritten — ${esc(result.style||'balanced').toUpperCase()}</span>
|
||||
<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-optimised title</span>
|
||||
<p style="font-size:.84rem;color:var(--accent2);margin:.5rem 0">${esc(result.subtitle)}</p>
|
||||
|
||||
<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:.75rem">${rBullets}</div>
|
||||
|
||||
<div style="margin-top:.75rem;padding:.6rem .8rem;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);border-radius:8px;font-size:.84rem;color:var(--gold)">⭐ ${esc(result.social_proof)}</div>
|
||||
<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:.75rem;font-size:1.05rem;font-weight:800;color:var(--accent)">${esc(result.price_reframe)}</div>
|
||||
<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:.75rem;font-size:.84rem;color:var(--text2);font-style:italic">${esc(result.usage_instruction)}</p>
|
||||
<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:1rem;text-align:center">
|
||||
<button style="background:var(--accent);color:#060a0f;border:none;padding:.7rem 2rem;border-radius:8px;font-size:.9rem;font-weight:700;cursor:pointer">${esc(result.cta_text || 'Add to Basket')}</button>
|
||||
<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 nav
|
||||
// Smooth scroll
|
||||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', e => {
|
||||
const t = document.querySelector(a.getAttribute('href'));
|
||||
|
||||
Reference in New Issue
Block a user