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:
2026-03-02 20:41:30 +08:00
parent 3f2b6b6188
commit 7e58ab1970
4 changed files with 687 additions and 354 deletions

View File

@@ -1,20 +1,18 @@
"""AI engine — Gemini for copy, Nano Banana / Nano Banana Pro for imagery.
"""AI engine — Gemini for copy, Nano Banana / Pro for image-to-image product shots.
Models used:
Text: gemini-2.5-flash — all copy generation
Image: gemini-2.5-flash-image — Nano Banana (fast lifestyle shots)
Image: gemini-3-pro-image-preview — Nano Banana Pro (premium hero/product shots)
Image generation uses the REAL scraped product image as a reference.
Gemini receives the actual photo and generates ecommerce-grade variations
that maintain product consistency.
Powers:
Demo A: generate_asset_pack() — 1 product → 12 marketing assets
Demo B: competitor_xray() — competitor URL → analysis + JV upgrade
Demo C: pdp_surgeon() — existing copy → style variants
PDP: optimise_pdp_copy() — full PDP rewrite
Images: generate_all_images() — Nano Banana product imagery
Models:
Text: gemini-2.5-flash
Image: gemini-2.5-flash-image — Nano Banana (fast edits)
Image: gemini-3-pro-image-preview — Nano Banana Pro (premium product photography)
"""
import os, json, hashlib, re
import os, json, hashlib, re, io, time
from pathlib import Path
import requests as http_requests
from google import genai
from google.genai import types
@@ -28,6 +26,13 @@ TEXT_MODEL = "gemini-2.5-flash"
IMG_FAST = "gemini-2.5-flash-image" # Nano Banana
IMG_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
# ── Helpers ──────────────────────────────────────────────────
def _call_gemini(prompt: str, temperature: float = 0.7) -> dict:
"""Call Gemini text model, return parsed JSON."""
@@ -50,14 +55,30 @@ def _call_gemini(prompt: str, temperature: float = 0.7) -> dict:
return {"error": "Failed to parse AI response", "raw": response.text[:500]}
def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
"""Generate image via Nano Banana. Returns (filename, mime_type) or ('','')."""
if not client:
def _download_image(url: str) -> tuple:
"""Download image, return (bytes, mime_type) or (None, None)."""
try:
r = http_requests.get(url, headers=HEADERS, timeout=15)
r.raise_for_status()
ct = r.headers.get("content-type", "image/jpeg")
mime = ct.split(";")[0].strip()
if "image" not in mime:
mime = "image/jpeg"
return (r.content, mime)
except Exception as e:
print(f"[download] Failed {url}: {e}")
return (None, None)
def _generate_image_from_ref(prompt: str, ref_bytes: bytes, ref_mime: str,
model: str = IMG_PRO) -> tuple:
"""Generate image using a real product photo as reference.
Returns (filename, mime_type) or ('', '')."""
if not client or not ref_bytes:
return ("", "")
cache_key = hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()[:14]
# Check cache
for ext in ("png", "jpg", "jpeg"):
cache_key = hashlib.md5(f"{model}:{prompt}:{hashlib.md5(ref_bytes).hexdigest()[:8]}".encode()).hexdigest()[:16]
for ext in ("png", "jpg", "jpeg", "webp"):
cached = GEN_DIR / f"{cache_key}.{ext}"
if cached.exists():
return (cached.name, f"image/{ext}")
@@ -65,7 +86,10 @@ def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
try:
response = client.models.generate_content(
model=model,
contents=prompt,
contents=[
types.Part.from_bytes(data=ref_bytes, mime_type=ref_mime),
prompt,
],
config=types.GenerateContentConfig(
response_modalities=["IMAGE", "TEXT"],
),
@@ -82,74 +106,268 @@ def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
return ("", "")
def _generate_image_text_only(prompt: str, model: str = IMG_PRO) -> tuple:
"""Fallback: text-only image generation when no reference available."""
if not client:
return ("", "")
cache_key = hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()[:16]
for ext in ("png", "jpg", "jpeg", "webp"):
cached = GEN_DIR / f"{cache_key}.{ext}"
if cached.exists():
return (cached.name, f"image/{ext}")
try:
response = client.models.generate_content(
model=model,
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=["IMAGE", "TEXT"],
),
)
for part in response.candidates[0].content.parts:
if part.inline_data and part.inline_data.data:
mime = part.inline_data.mime_type or "image/png"
ext = "jpg" if "jpeg" in mime or "jpg" in mime else "png"
filename = f"{cache_key}.{ext}"
(GEN_DIR / filename).write_bytes(part.inline_data.data)
return (filename, mime)
except Exception as e:
print(f"[img-gen-fallback] {model} error: {e}")
return ("", "")
# ═══════════════════════════════════════════════════════════════
# DEMO A — One Product → 12 Assets
# DEMO A — Conversion-Optimised PDP + Asset Pack
# ═══════════════════════════════════════════════════════════════
def generate_asset_pack(product: dict) -> dict:
prompt = f"""You are a world-class ecommerce copywriter and marketing strategist for Just Vitamins (justvitamins.co.uk), a trusted UK vitamin brand — 4.8★ Trustpilot, 230,000+ customers, 20 years trading.
"""Generate a full conversion-optimised PDP structure.
The output maps directly to a real product detail page layout:
hero section, image gallery context, benefit bullets, trust bar,
persuasion copy, FAQ, meta SEO, ad hooks, email subjects.
"""
prompt = f"""You are a senior ecommerce conversion strategist who has optimised PDPs for brands doing £50M+/yr.
You're writing for Just Vitamins (justvitamins.co.uk) — a trusted UK supplement brand:
- 4.8★ Trustpilot (230,000+ customers)
- 20 years trading
- Eco bio-pouch packaging
- UK-made, GMP certified
PRODUCT DATA:
- Title: {product.get('title','')}
- Subtitle: {product.get('subtitle','')}
- Price: {product.get('price','')} for {product.get('quantity','')}
- Per unit: {product.get('per_unit_cost','')}
- Benefits: {json.dumps(product.get('benefits',[]))}
- Description: {product.get('description','')[:1500]}
- EFSA Health Claims: {json.dumps(product.get('health_claims',[]))}
- Category: {product.get('category','')}
- Benefits: {json.dumps(product.get('benefits',[]))}
- Description: {product.get('description','')[:2000]}
- EFSA Health Claims: {json.dumps(product.get('health_claims',[]))}
Generate a COMPLETE 12-asset marketing pack. Be specific to THIS product.
Generate a conversion-optimised PDP structure following these ecommerce best practices:
1. ABOVE THE FOLD: Hero headline + value prop must stop the scroll
2. GALLERY CONTEXT: Captions for each product image (main, lifestyle, scale, ingredients)
3. BENEFIT BULLETS: Feature → Benefit → Proof format. Max 6, icon-ready.
4. TRUST BAR: 4 trust signals (Trustpilot, years, GMP, eco)
5. PERSUASION SECTION: "Why this formula" — science-led, 2-3 short paragraphs
6. SOCIAL PROOF: Real-format review quote + stats
7. PRICE FRAMING: Reframe as daily cost, compare to coffee/etc
8. URGENCY: Ethical scarcity or time-based nudge
9. FAQ: 4 questions that handle real objections (dosage, interactions, results timeline, quality)
10. CTA: Primary + secondary button text
11. META SEO: Title <60 chars, description <155 chars, primary keyword
12. AD HOOKS: 5 scroll-stopping hooks for Meta/TikTok ads
13. EMAIL: 3 subject lines with preview text for welcome/abandon/restock flows
Return JSON:
{{
"hero_angles": [
{{"headline":"","target_desire":"","best_for":""}},
{{"headline":"","target_desire":"","best_for":""}},
{{"headline":"","target_desire":"","best_for":""}}
"pdp": {{
"hero_headline": "Conversion-optimised H1",
"hero_subhead": "One sentence that makes them stay",
"value_props": ["Short prop 1", "Short prop 2", "Short prop 3"],
"gallery_captions": {{
"main": "What the buyer sees first — describe ideal main shot context",
"lifestyle": "Describe the lifestyle scene this product belongs in",
"scale": "Describe a scale/size reference shot",
"ingredients": "Describe the close-up ingredients/label shot"
}},
"benefit_bullets": [
{{"icon": "emoji", "headline": "Short benefit", "detail": "Why it matters to the buyer", "proof": "Clinical or data point"}},
{{"icon": "emoji", "headline": "", "detail": "", "proof": ""}},
{{"icon": "emoji", "headline": "", "detail": "", "proof": ""}},
{{"icon": "emoji", "headline": "", "detail": "", "proof": ""}},
{{"icon": "emoji", "headline": "", "detail": "", "proof": ""}}
],
"trust_signals": [
{{"icon": "", "text": "4.8★ Trustpilot"}},
{{"icon": "🏆", "text": "20 Years Trusted"}},
{{"icon": "🇬🇧", "text": "UK Made · GMP"}},
{{"icon": "🌿", "text": "Eco Bio-Pouch"}}
],
"why_section_title": "Why This Formula",
"why_paragraphs": ["Science paragraph 1", "Absorption/bioavailability paragraph 2", "Who benefits paragraph 3"],
"review_quote": {{"text": "Realistic-sounding 5★ review", "author": "Verified Buyer name", "stars": 5}},
"stats_bar": [
{{"number": "230,000+", "label": "Happy Customers"}},
{{"number": "4.8★", "label": "Trustpilot Rating"}},
{{"number": "20+", "label": "Years Trusted"}}
],
"price_display": {{
"main_price": "{product.get('price','')}",
"price_per_day": "Daily cost calculation",
"comparison": "That's less than a daily coffee",
"savings_note": "Subscribe & save note if applicable"
}},
"urgency_note": "Ethical urgency message",
"cta_primary": "Primary button text",
"cta_secondary": "Secondary action text",
"faq": [
{{"q": "Dosage question", "a": "Clear answer"}},
{{"q": "Results timeline question", "a": "Honest answer"}},
{{"q": "Quality/safety question", "a": "Trust-building answer"}},
{{"q": "Interaction question", "a": "Responsible answer"}}
],
"usage_instructions": "Simple, friction-free how-to-take instructions"
}},
"meta_seo": {{
"title": "SEO title under 60 chars",
"description": "Meta description under 155 chars with primary keyword",
"primary_keyword": "target keyword"
}},
"ad_hooks": [
{{"hook": "Scroll-stopping first line", "angle": "What desire it targets", "platform": "Meta/TikTok/Google"}},
{{"hook": "", "angle": "", "platform": ""}},
{{"hook": "", "angle": "", "platform": ""}},
{{"hook": "", "angle": "", "platform": ""}},
{{"hook": "", "angle": "", "platform": ""}}
],
"pdp_copy": {{
"headline":"",
"bullets":["","","","",""],
"faq":[{{"q":"","a":""}},{{"q":"","a":""}},{{"q":"","a":""}}]
}},
"ad_hooks":["","","","",""],
"email_subjects":[
{{"subject":"","preview":""}},
{{"subject":"","preview":""}},
{{"subject":"","preview":""}}
],
"tiktok_script":{{
"title":"",
"hook_0_3s":"",
"body_3_12s":"",
"cta_12_15s":"",
"why_it_works":""
}},
"blog_outline":{{
"title":"",
"sections":["","","","",""],
"seo_keyword":"",
"monthly_searches":""
}},
"meta_seo":{{
"title":"","description":"","title_chars":0,"desc_chars":0
}},
"alt_text":[
{{"image_type":"Hero product shot","alt":"","filename":""}},
{{"image_type":"Lifestyle image","alt":"","filename":""}}
],
"ab_variants":[
{{"label":"Rational","copy":""}},
{{"label":"Emotional","copy":""}},
{{"label":"Social Proof","copy":""}}
"email_subjects": [
{{"flow": "Welcome", "subject": "Under 50 chars", "preview": "Preview text"}},
{{"flow": "Abandon Cart", "subject": "", "preview": ""}},
{{"flow": "Restock", "subject": "", "preview": ""}}
]
}}
RULES: EFSA claims must be accurate. Email subjects <50 chars. Meta title <60 chars, description <160 chars. UK English."""
RULES:
- EFSA health claims only — no therapeutic claims
- UK English
- Be specific to THIS product — no generic filler
- Every bullet must pass the "so what?" test
- Price framing must use real numbers from the data"""
return _call_gemini(prompt, 0.75)
# ═══════════════════════════════════════════════════════════════
# IMAGE GENERATION — Reference-based product photography
# ═══════════════════════════════════════════════════════════════
def generate_product_images(product: dict) -> dict:
"""Generate ecommerce product images using the REAL scraped product photo.
Downloads the actual product image from justvitamins.co.uk, sends it
to Gemini as visual reference, and generates conversion-optimised
variations that maintain product consistency.
"""
title = product.get("title", "vitamin supplement")
images = product.get("images", [])
results = {"original_images": images}
# Download the primary product image as reference
ref_bytes, ref_mime = None, None
for img_url in images:
ref_bytes, ref_mime = _download_image(img_url)
if ref_bytes:
# Save the original too
orig_hash = hashlib.md5(ref_bytes).hexdigest()[:12]
ext = "jpg" if "jpeg" in ref_mime else "png"
orig_name = f"orig_{orig_hash}.{ext}"
orig_path = GEN_DIR / orig_name
if not orig_path.exists():
orig_path.write_bytes(ref_bytes)
results["original"] = {"filename": orig_name, "mime": ref_mime, "source": img_url}
break
if not ref_bytes:
results["error"] = "Could not download product image from source"
return results
# ── 1. HERO: Clean white-background product shot ─────────
hero_prompt = (
"You are a professional ecommerce product photographer. "
"Take this exact product and create a clean, premium product photograph. "
"KEEP THE EXACT SAME PRODUCT — same packaging, same label, same colours. "
"Place it on a pure white background with soft studio lighting and subtle shadow. "
"The product should be centred, well-lit, and sharp. "
"This is the main product image for an online store — it must look professional "
"and match Amazon/Shopify product photography standards. "
"Do NOT change the product, do NOT add text, do NOT alter the packaging."
)
fname, mime = _generate_image_from_ref(hero_prompt, ref_bytes, ref_mime, IMG_PRO)
results["hero"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro",
"caption": "Main product shot — clean white background, studio lighting"}
# ── 2. LIFESTYLE: Product in real-life setting ───────────
lifestyle_prompt = (
"You are a lifestyle product photographer for a premium health brand. "
"Take this exact supplement product and photograph it in a beautiful morning "
"kitchen scene. Place the EXACT SAME product on a light marble countertop "
"with morning sunlight, a glass of water, and fresh green leaves nearby. "
"Warm, healthy, inviting mood. Shallow depth of field with product in sharp focus. "
"KEEP the product exactly as it is — same packaging, same label. "
"Do NOT change, redesign, or alter the product in any way. "
"Professional lifestyle product photography for an ecommerce website."
)
fname, mime = _generate_image_from_ref(lifestyle_prompt, ref_bytes, ref_mime, IMG_PRO)
results["lifestyle"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro",
"caption": "Lifestyle shot — morning kitchen scene, natural light"}
# ── 3. SCALE: Product with hand/everyday object ──────────
scale_prompt = (
"You are a product photographer creating a scale reference image. "
"Show this exact supplement product being held in a person's hand, "
"or placed next to a coffee mug for size reference. "
"The product must be the EXACT SAME — same packaging, same label, same design. "
"Clean, bright lighting. Natural skin tones. "
"This helps online shoppers understand the actual size of the product. "
"Do NOT modify, redesign, or change the product appearance."
)
fname, mime = _generate_image_from_ref(scale_prompt, ref_bytes, ref_mime, IMG_FAST)
results["scale"] = {"filename": fname, "mime": mime, "model": "Nano Banana",
"caption": "Scale reference — real-world size context"}
# ── 4. INGREDIENTS: Close-up detail shot ─────────────────
ingredients_prompt = (
"You are a detail product photographer. "
"Create a close-up macro shot of this supplement product, focusing on the "
"label, ingredients panel, or the capsules/tablets spilling out of the packaging. "
"KEEP the exact same product — same packaging, same branding. "
"Sharp focus on details, soft bokeh background. "
"Premium, trustworthy feel — suitable for a health brand website. "
"Do NOT change the product design or branding."
)
fname, mime = _generate_image_from_ref(ingredients_prompt, ref_bytes, ref_mime, IMG_FAST)
results["ingredients"] = {"filename": fname, "mime": mime, "model": "Nano Banana",
"caption": "Detail shot — ingredients & quality close-up"}
# ── 5. BANNER: Wide hero banner for category/landing page ─
banner_prompt = (
"You are creating a wide ecommerce hero banner image. "
"Place this exact supplement product on the right side of a wide composition. "
"The left side should have clean space for text overlay (no text in the image). "
"Use a soft gradient background in natural greens and creams. "
"Include subtle natural elements — leaves, light rays, bokeh. "
"KEEP the product exactly as it is — same packaging, same label. "
"Wide aspect ratio, suitable for a website hero banner. "
"Do NOT add any text, logos, or modify the product."
)
fname, mime = _generate_image_from_ref(banner_prompt, ref_bytes, ref_mime, IMG_PRO)
results["banner"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro",
"caption": "Hero banner — wide format for landing pages"}
return results
# ═══════════════════════════════════════════════════════════════
# DEMO B — Competitor X-Ray
# ═══════════════════════════════════════════════════════════════
@@ -201,51 +419,58 @@ RULES: No false claims. EFSA/ASA compliant. Strategic, not aggressive."""
# ═══════════════════════════════════════════════════════════════
STYLE_INSTRUCTIONS = {
"balanced": "Balanced, trustworthy DTC supplement voice. Mix emotional hooks with rational proof.",
"premium": "Premium aspirational voice. Sophisticated language, formulation science, target affluent buyers.",
"dr": "Direct-response style. Pattern interrupts, urgency, specific numbers, scarcity, stacked bonuses.",
"medical": "Clinical, medically-safe tone. Proper nomenclature, structure/function claims only, FDA disclaimer.",
"balanced": "Balanced, trustworthy DTC supplement voice. Mix emotional hooks with rational proof. Think Huel or Athletic Greens.",
"premium": "Premium aspirational voice. Sophisticated language, formulation science focus, target affluent health-conscious buyers. Think Lyma or Seed.",
"dr": "Direct-response style. Pattern interrupts, urgency, specific numbers, stacked value, scarcity. Think agora-style health copy.",
"medical": "Clinical, medically-safe tone. Proper nomenclature, structure/function claims only, evidence citations. Think Thorne or Pure Encapsulations.",
}
def pdp_surgeon(product: dict, style: str = "balanced") -> dict:
instruction = STYLE_INSTRUCTIONS.get(style, STYLE_INSTRUCTIONS["balanced"])
prompt = f"""You are a PDP conversion specialist rewriting a product page for Just Vitamins.
prompt = f"""You are an elite PDP conversion specialist. You've increased CVR by 30-80% for DTC supplement brands.
PRODUCT:
- Title: {product.get('title','')}
- Subtitle: {product.get('subtitle','')}
- Price: {product.get('price','')} for {product.get('quantity','')}
- Per unit: {product.get('per_unit_cost','')}
- Benefits: {json.dumps(product.get('benefits',[]))}
- Description: {product.get('description','')[:1500]}
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
STYLE: {style.upper()}{instruction}
Rewrite the PDP. For EVERY change add a conversion annotation with estimated % impact.
Rewrite the entire PDP in this style. For EVERY element, add a conversion annotation explaining the psychology and estimated lift.
Output JSON:
{{
"style":"{style}",
"title":"",
"subtitle":"",
"hero_copy":"Main persuasion paragraph",
"hero_annotation":"Why this works — conversion impact",
"title":"Conversion-optimised title",
"subtitle":"Desire-triggering subtitle",
"hero_copy":"2-3 sentence persuasion paragraph — the most important copy on the page",
"hero_annotation":"Why this works — which conversion principle, estimated % lift",
"bullets":[
{{"text":"","annotation":"Why — e.g. +22% add-to-cart"}},
{{"text":"Feature → Benefit → Proof bullet","annotation":"Conversion principle + estimated lift"}},
{{"text":"","annotation":""}},
{{"text":"","annotation":""}},
{{"text":"","annotation":""}},
{{"text":"","annotation":""}}
],
"social_proof":"Line using 4.8★ Trustpilot, 230K customers",
"social_proof_annotation":"",
"price_reframe":"Reframe price as no-brainer",
"price_annotation":"",
"cta_text":"CTA button text",
"usage_instruction":"How to take — written to remove friction",
"usage_annotation":""
"social_proof":"Specific social proof line using 4.8★ Trustpilot, 230K customers, years trading",
"social_proof_annotation":"Which social proof principle this uses + estimated lift",
"price_reframe":"Reframe the price as a no-brainer — use daily cost, comparison anchoring",
"price_annotation":"Price psychology principle + estimated lift",
"cta_text":"CTA button text — action-oriented, benefit-driven",
"cta_annotation":"Why this CTA works",
"usage_instruction":"How to take — written to build routine and reduce friction",
"usage_annotation":"How this reduces returns and increases LTV"
}}
RULES: EFSA claims accurate. Realistic % lifts (5-40%). UK English."""
RULES:
- EFSA claims only — no disease claims, no cure claims
- Realistic lift estimates (5-40% range)
- UK English
- Every annotation must cite a specific conversion principle (Cialdini, Kahneman, Fogg, etc.)"""
return _call_gemini(prompt, 0.8)
@@ -254,17 +479,15 @@ RULES: EFSA claims accurate. Realistic % lifts (5-40%). UK English."""
# ═══════════════════════════════════════════════════════════════
def optimise_pdp_copy(product: dict) -> dict:
prompt = f"""You are an expert ecommerce copywriter for Just Vitamins (justvitamins.co.uk) — 4.8★ Trustpilot, 230K+ customers, 20 years.
prompt = f"""You are an expert ecommerce copywriter for Just Vitamins — 4.8★ Trustpilot, 230K+ customers, 20 years.
PRODUCT:
- Title: {product['title']}
- Subtitle: {product.get('subtitle','')}
- Price: {product.get('price','')} for {product.get('quantity','')}
- Per unit: {product.get('per_unit_cost','')}
- Benefits: {json.dumps(product.get('benefits',[]))}
- Description: {product.get('description','')[:1500]}
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
- Category: {product.get('category','')}
Rewrite everything. Output JSON:
{{
@@ -278,51 +501,5 @@ Rewrite everything. Output JSON:
"faqs":[{{"q":"","a":""}},{{"q":"","a":""}},{{"q":"","a":""}}]
}}
Keep EFSA claims accurate. UK English."""
EFSA claims only. UK English."""
return _call_gemini(prompt, 0.7)
# ═══════════════════════════════════════════════════════════════
# IMAGE GENERATION — Nano Banana / Pro
# ═══════════════════════════════════════════════════════════════
def generate_product_images(product: dict) -> dict:
"""Generate product images using Nano Banana (fast) + Nano Banana Pro (hero)."""
title = product.get("title", "vitamin supplement")
category = product.get("category", "vitamins")
results = {}
# Hero — use Nano Banana Pro for best quality
hero_prompt = (
f"Professional product hero photograph for an ecommerce page. "
f"A premium eco-friendly kraft bio-pouch supplement packaging for '{title}' "
f"by Just Vitamins UK. Clean cream/white background, soft natural shadows, "
f"minimalist staging with a small ceramic dish of capsules/tablets beside it "
f"and a sprig of green herb. Premium, trustworthy, modern. "
f"Commercial product photography, sharp focus, studio lighting."
)
fname, mime = _generate_image(hero_prompt, IMG_PRO)
results["hero"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro"}
# Lifestyle — Nano Banana (fast)
lifestyle_prompt = (
f"Lifestyle product photograph: '{title}' by Just Vitamins UK in an eco "
f"bio-pouch sitting on a marble kitchen counter with morning sunlight "
f"streaming through a window. Fresh fruit, a glass of water, and green "
f"leaves nearby. Warm, healthy, inviting mood. Shallow depth of field. "
f"Photorealistic, 4K quality."
)
fname, mime = _generate_image(lifestyle_prompt, IMG_FAST)
results["lifestyle"] = {"filename": fname, "mime": mime, "model": "Nano Banana"}
# Benefits — Nano Banana Pro
benefits_prompt = (
f"Clean infographic-style illustration showing health benefits of "
f"{category} supplements: strong bones, immune support, energy, vitality. "
f"Modern flat design, warm gold/green/cream palette. "
f"Professional, suitable for a premium UK health brand website. No text."
)
fname, mime = _generate_image(benefits_prompt, IMG_PRO)
results["benefits"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro"}
return results

View File

@@ -144,6 +144,116 @@ h1{font-size:clamp(2.2rem,5.5vw,3.5rem);font-weight:800;line-height:1.08;letter-
footer{padding:1.5rem 0;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--muted)}
footer a{color:var(--accent);text-decoration:none}
/* ═══ PDP LAYOUT ═══ */
.pdp-page{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;animation:fadeUp .5s ease}
/* Above the fold — gallery + info */
.pdp-atf{display:grid;grid-template-columns:1fr 1fr;gap:0}
.pdp-gallery{background:var(--bg);padding:1.5rem;border-right:1px solid var(--border)}
.pdp-info{padding:1.5rem 2rem}
.pdp-gallery-main{position:relative;border-radius:10px;overflow:hidden;background:#0a0e14;margin-bottom:.75rem;aspect-ratio:1;display:flex;align-items:center;justify-content:center}
.pdp-gallery-main img{max-width:100%;max-height:100%;object-fit:contain}
.pdp-gallery-badge{position:absolute;top:.6rem;left:.6rem;font-family:'JetBrains Mono',monospace;font-size:.6rem;font-weight:700;padding:.2rem .5rem;border-radius:4px;background:rgba(6,10,15,.85);color:var(--accent);border:1px solid var(--border)}
.pdp-thumbs{display:flex;gap:.5rem;overflow-x:auto;padding:.25rem 0}
.pdp-thumb{width:72px;height:72px;border-radius:8px;overflow:hidden;cursor:pointer;border:2px solid transparent;position:relative;flex-shrink:0;background:var(--bg);transition:.2s}
.pdp-thumb.wide{width:110px}
.pdp-thumb.active{border-color:var(--accent)}
.pdp-thumb:hover{border-color:var(--border2)}
.pdp-thumb img{width:100%;height:100%;object-fit:cover}
.pdp-thumb-label{position:absolute;bottom:0;left:0;right:0;font-size:.5rem;font-weight:700;text-align:center;padding:.12rem;font-family:'JetBrains Mono',monospace;background:rgba(6,10,15,.85)}
.pdp-thumb-label.orig{color:var(--muted)}
.pdp-thumb-label.ai{color:var(--cyan)}
/* Info panel */
.pdp-breadcrumb{font-family:'JetBrains Mono',monospace;font-size:.68rem;color:var(--muted);margin-bottom:.5rem}
.pdp-title{font-size:1.35rem;font-weight:700;line-height:1.25;margin-bottom:.4rem}
.pdp-subhead{font-size:.9rem;color:var(--text2);margin-bottom:.75rem;line-height:1.5}
.pdp-value-props{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:1rem}
.pdp-vp{font-size:.72rem;font-weight:600;color:var(--accent);background:var(--glow);padding:.2rem .5rem;border-radius:4px}
/* Price block */
.pdp-price-block{background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;margin-bottom:1rem}
.pdp-price-main{font-size:1.8rem;font-weight:800;color:var(--text)}
.pdp-price-qty{font-size:.78rem;color:var(--muted);margin-left:.3rem}
.pdp-price-daily{font-size:.82rem;color:var(--accent);font-weight:600;margin-top:.25rem}
.pdp-price-compare{font-size:.75rem;color:var(--muted);margin-top:.1rem}
/* Trust bar */
.pdp-trust-bar{display:flex;gap:.6rem;margin-bottom:1rem;flex-wrap:wrap}
.pdp-trust-item{display:flex;align-items:center;gap:.25rem;font-size:.72rem;font-weight:600;color:var(--text2);background:var(--bg);padding:.3rem .6rem;border-radius:6px;border:1px solid var(--border)}
.pdp-trust-icon{font-size:.85rem}
/* CTAs */
.pdp-cta-primary{width:100%;padding:.8rem;font-size:.95rem;font-weight:700;border:none;border-radius:9px;cursor:pointer;background:var(--accent);color:#060a0f;margin-bottom:.5rem;transition:.2s}
.pdp-cta-primary:hover{background:var(--accent2);transform:translateY(-1px)}
.pdp-cta-secondary{width:100%;padding:.6rem;font-size:.82rem;font-weight:600;border:1px solid var(--border2);border-radius:9px;cursor:pointer;background:transparent;color:var(--text2);margin-bottom:.75rem;transition:.2s}
.pdp-cta-secondary:hover{border-color:var(--accent);color:var(--accent)}
.pdp-urgency{font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--gold);background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);padding:.4rem .7rem;border-radius:6px;margin-bottom:.75rem;text-align:center}
.pdp-usage{font-size:.78rem;color:var(--muted);line-height:1.6;padding-top:.5rem;border-top:1px solid var(--border)}
/* Sections below fold */
.pdp-section{padding:2rem 2.5rem;border-top:1px solid var(--border)}
.pdp-section h3{font-size:1.1rem;font-weight:700;margin-bottom:1rem}
/* Benefit bullets */
.pdp-bullets{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.pdp-bullet{display:flex;gap:.6rem;padding:.75rem;background:var(--bg);border:1px solid var(--border);border-radius:9px}
.pdp-bullet-icon{font-size:1.3rem;flex-shrink:0;margin-top:.1rem}
.pdp-bullet strong{font-size:.84rem;color:var(--text)}
.pdp-bullet-detail{font-size:.78rem;color:var(--text2);display:block;margin-top:.15rem}
.pdp-bullet-proof{font-family:'JetBrains Mono',monospace;font-size:.65rem;color:var(--cyan);display:block;margin-top:.25rem}
/* Stats bar */
.pdp-stats-bar{display:flex;justify-content:center;gap:3rem;padding:1.5rem 2rem;background:var(--bg);border-top:1px solid var(--border)}
.pdp-stat{text-align:center}
.pdp-stat-num{font-family:'JetBrains Mono',monospace;font-size:1.4rem;font-weight:800;color:var(--accent);display:block}
.pdp-stat-label{font-size:.7rem;color:var(--muted)}
/* Why section */
.pdp-why p{font-size:.88rem;color:var(--text2);line-height:1.7;margin-bottom:.75rem}
/* Review */
.pdp-review-section{padding:2rem 2.5rem;border-top:1px solid var(--border);background:var(--bg)}
.pdp-review-card{max-width:600px;margin:0 auto;text-align:center}
.pdp-review-stars{font-size:1.3rem;color:var(--gold);margin-bottom:.5rem;letter-spacing:2px}
.pdp-review-text{font-size:.95rem;color:var(--text2);line-height:1.6;font-style:italic;margin-bottom:.5rem}
.pdp-review-author{font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--muted)}
/* FAQ */
.pdp-faq{max-width:700px}
.pdp-faq-item{border:1px solid var(--border);border-radius:9px;margin-bottom:.5rem;overflow:hidden}
.pdp-faq-item summary{padding:.75rem 1rem;font-size:.88rem;font-weight:600;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between}
.pdp-faq-item summary::after{content:'';font-size:.9rem;color:var(--muted)}
.pdp-faq-item[open] summary::after{content:''}
.pdp-faq-item p{padding:0 1rem .75rem;font-size:.82rem;color:var(--text2);line-height:1.6}
/* Divider */
.pdp-divider{text-align:center;padding:1.5rem;border-top:1px solid var(--border);font-family:'JetBrains Mono',monospace;font-size:.75rem;color:var(--muted)}
/* Asset sections */
.pdp-asset-section{padding:1.5rem 2.5rem;border-top:1px solid var(--border)}
.pdp-asset-section h4{font-size:.9rem;font-weight:700;margin-bottom:.75rem}
/* SEO preview */
.pdp-seo{background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;max-width:600px}
.pdp-seo-title{font-size:1.05rem;color:var(--blue);font-weight:500;margin-bottom:.1rem}
.pdp-seo-url{font-size:.72rem;color:var(--accent);margin-bottom:.25rem}
.pdp-seo-desc{font-size:.82rem;color:var(--text2);line-height:1.5}
/* Ad hooks */
.pdp-hooks-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}
.pdp-hook{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:.75rem}
.pdp-hook-text{font-size:.84rem;font-weight:600;color:var(--text);line-height:1.4;margin-bottom:.3rem}
.pdp-hook-meta{font-family:'JetBrains Mono',monospace;font-size:.6rem;color:var(--muted)}
/* Email */
.pdp-emails{display:flex;flex-direction:column;gap:.5rem}
.pdp-email{display:flex;align-items:baseline;gap:.6rem;padding:.6rem .8rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:.82rem}
.pdp-email-flow{font-family:'JetBrains Mono',monospace;font-size:.6rem;font-weight:700;color:var(--purple);text-transform:uppercase;background:rgba(139,92,246,.12);padding:.15rem .4rem;border-radius:3px;flex-shrink:0}
.pdp-email strong{color:var(--text)}
.pdp-email-preview{color:var(--muted);font-size:.75rem}
/* Responsive */
@media(max-width:900px){.pdp-atf{grid-template-columns:1fr}.pdp-gallery{border-right:none;border-bottom:1px solid var(--border)}.pdp-bullets{grid-template-columns:1fr}.pdp-hooks-grid{grid-template-columns:1fr}.pdp-stats-bar{gap:1.5rem}}
@media(max-width:800px){.input-row{flex-direction:column}.btn-gen{width:100%}.split{grid-template-columns:1fr}.split-left{border-right:none;border-bottom:1px solid var(--border)}.stats{gap:1.25rem}.step-bar{flex-direction:column;gap:.4rem}footer{flex-direction:column;gap:.3rem;text-align:center}}
@media(max-width:500px){.nav-links a{font-size:.68rem;padding:.25rem .4rem}.btn-row{flex-direction:column}.btn{width:100%;justify-content:center}}
@media(max-width:500px){.nav-links a{font-size:.68rem;padding:.25rem .4rem}.btn-row{flex-direction:column}.btn{width:100%;justify-content:center}.pdp-section{padding:1.25rem 1rem}.pdp-asset-section{padding:1.25rem 1rem}}

View File

@@ -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'));

View File

@@ -47,8 +47,8 @@
<!-- ═══ DEMO A — 12 Assets ═══ -->
<section class="demo" id="demo-a">
<span class="badge red">⚡ DEMO A — LIVE</span>
<h2>One Product → 12 Assets in Seconds</h2>
<p class="sub">Paste a real Just Vitamins product URL. Gemini scrapes it, then generates a full marketing pack.</p>
<h2>One Product URL → Full Conversion PDP</h2>
<p class="sub">Paste a real Just Vitamins product URL. We scrape the actual product images, then Gemini generates a complete conversion-optimised PDP with real product photography variations — not invented images.</p>
<div class="input-card">
<div class="input-row">
@@ -56,23 +56,17 @@
<label>PRODUCT URL</label>
<input type="url" id="demoA-url" placeholder="https://www.justvitamins.co.uk/..." value="https://www.justvitamins.co.uk/Bone-Health/Super-Strength-Vitamin-D3-4000iu-K2-MK-7-100mcg.aspx">
</div>
<button class="btn-gen red" id="demoA-btn" onclick="runDemoA()">🔴 Generate the Whole Pack</button>
<button class="btn-gen red" id="demoA-btn" onclick="runDemoA()">🔴 Generate Full PDP</button>
</div>
<div class="step-bar" id="demoA-steps">
<div class="step" id="a-step-scrape"><span class="step-label">1. Scrape</span><span class="step-status" id="a-s1">Waiting</span></div>
<div class="step" id="a-step-ai"><span class="step-label">2. AI Generate</span><span class="step-status" id="a-s2">Waiting</span></div>
<div class="step" id="a-step-images"><span class="step-label">3. Image Gen</span><span class="step-status" id="a-s3">Waiting</span></div>
<div class="step" id="a-step-ai"><span class="step-label">2. PDP Copy</span><span class="step-status" id="a-s2">Waiting</span></div>
<div class="step" id="a-step-images"><span class="step-label">3. Product Photos</span><span class="step-status" id="a-s3">Waiting</span></div>
</div>
</div>
<div id="demoA-output" class="output hidden">
<div class="output-meta" id="demoA-meta"></div>
<div class="asset-grid" id="demoA-assets"></div>
<div id="demoA-images" class="img-section hidden">
<h3>🎨 AI-Generated Product Images</h3>
<p class="sub-sm">Generated with Nano Banana / Nano Banana Pro — unique images that don't exist anywhere else.</p>
<div class="img-grid" id="demoA-img-grid"></div>
</div>
<div id="demoA-pdp"></div>
</div>
</section>