- Flask + gunicorn backend replacing static nginx - 3 live AI demos powered by Gemini 2.5 Flash - Nano Banana + Nano Banana Pro for product image generation - Real JV ecommerce dashboard (728K orders, 230K customers, 4MB data) - AI Infrastructure Proposal + Offer pages - Live product scraper for justvitamins.co.uk + competitor pages - API: /api/scrape, /api/generate-pack, /api/competitor-xray, /api/pdp-surgeon, /api/generate-images
403 lines
16 KiB
JavaScript
403 lines
16 KiB
JavaScript
/* JustVitamin × QuikCue — Live AI Demos */
|
||
|
||
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 — One Product → 12 Assets + Images
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
let demoA_product = 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', '<span class="spinner"></span> Scraping...');
|
||
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,35))}...`);
|
||
} 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...');
|
||
|
||
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());
|
||
|
||
// Handle pack
|
||
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');
|
||
} 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);
|
||
} catch(e) {
|
||
setStep('#a-s3', 'error', `✗ ${esc(e.message)}`);
|
||
}
|
||
|
||
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;
|
||
|
||
// 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);
|
||
});
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
grid.innerHTML = cards;
|
||
}
|
||
|
||
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>`;
|
||
}
|
||
});
|
||
if (html) {
|
||
grid.innerHTML = html;
|
||
section.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// DEMO B — Competitor X-Ray
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
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-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', `✓ Scraped: ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
||
setStep('#b-s2', 'done', `✓ Analysis complete (${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) {
|
||
// Left — competitor analysis
|
||
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>
|
||
`;
|
||
|
||
// 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('');
|
||
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>
|
||
`;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// DEMO C — PDP Surgeon
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
let demoC_product = null;
|
||
let 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-s2', '', 'Waiting');
|
||
|
||
// Step 1: Scrape
|
||
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;
|
||
}
|
||
|
||
// Step 2: AI rewrite (default style)
|
||
const active = document.querySelector('#demoC-toggles .toggle.active');
|
||
const style = active?.dataset.style || 'balanced';
|
||
await rewriteStyle(style);
|
||
|
||
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', `<span class="spinner"></span> 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} rewrite (${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) {
|
||
// Left — current
|
||
const bullets = (product.benefits||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
||
$('#demoC-left').innerHTML = `
|
||
<span class="split-label bad">✕ Current PDP</span>
|
||
<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>
|
||
`;
|
||
|
||
// Right — rewritten
|
||
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">✓ AI-Rewritten — ${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>
|
||
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
`;
|
||
}
|
||
|
||
// Smooth scroll nav
|
||
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'}); }
|
||
});
|
||
});
|