Files
justvitamin/static/js/app.js
Omair Saleh 09d837a660 v2: Live Flask app — real Gemini AI demos, Nano Banana image gen, real £19.4M data dashboard
- 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
2026-03-02 20:02:25 +08:00

403 lines
16 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 */
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'}); }
});
});