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:
445
ai_engine.py
445
ai_engine.py
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
466
static/js/app.js
466
static/js/app.js
@@ -1,4 +1,5 @@
|
||||
/* JustVitamin × QuikCue — Live AI Demos */
|
||||
/* JustVitamin × QuikCue — Live AI Demos
|
||||
Demo A renders as a real conversion-optimised PDP page */
|
||||
|
||||
const $ = s => document.querySelector(s);
|
||||
const esc = s => { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
||||
@@ -9,7 +10,6 @@ function setStep(id, cls, text) {
|
||||
el.parentElement.className = `step ${cls}`;
|
||||
el.innerHTML = text;
|
||||
}
|
||||
|
||||
function setBtn(id, loading, text) {
|
||||
const btn = $(id);
|
||||
btn.disabled = loading;
|
||||
@@ -18,10 +18,12 @@ function setBtn(id, loading, text) {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DEMO A — One Product → 12 Assets + Images
|
||||
// DEMO A — Product URL → Full PDP + Real Product Images
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
let demoA_product = null;
|
||||
let demoA_pack = null;
|
||||
let demoA_imgs = null;
|
||||
|
||||
async function runDemoA() {
|
||||
const url = $('#demoA-url').value.trim();
|
||||
@@ -32,7 +34,7 @@ async function runDemoA() {
|
||||
['#a-s1','#a-s2','#a-s3'].forEach(s => setStep(s, '', 'Waiting'));
|
||||
|
||||
// Step 1: Scrape
|
||||
setStep('#a-s1', 'active', '<span class="spinner"></span> Scraping...');
|
||||
setStep('#a-s1', 'active', 'Scraping product page...');
|
||||
try {
|
||||
const r = await fetch('/api/scrape', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
@@ -41,16 +43,16 @@ async function runDemoA() {
|
||||
const product = await r.json();
|
||||
if (product.error) throw new Error(product.error);
|
||||
demoA_product = product;
|
||||
setStep('#a-s1', 'done', `✓ ${esc(product.title.substring(0,35))}...`);
|
||||
setStep('#a-s1', 'done', `✓ ${esc(product.title.substring(0,40))}...`);
|
||||
} catch(e) {
|
||||
setStep('#a-s1', 'error', `✗ ${esc(e.message)}`);
|
||||
setBtn('#demoA-btn', false, '🔴 Generate the Whole Pack');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: AI pack + Step 3: Images (parallel)
|
||||
setStep('#a-s2', 'active', '<span class="spinner"></span> Gemini generating 12 assets...');
|
||||
setStep('#a-s3', 'active', '<span class="spinner"></span> Nano Banana generating images...');
|
||||
// Step 2 + 3: AI copy + images in parallel
|
||||
setStep('#a-s2', 'active', 'Gemini generating PDP copy...');
|
||||
setStep('#a-s3', 'active', 'Generating product images from real photo...');
|
||||
|
||||
const packP = fetch('/api/generate-pack', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
@@ -62,164 +64,259 @@ async function runDemoA() {
|
||||
body: JSON.stringify(demoA_product)
|
||||
}).then(r => r.json());
|
||||
|
||||
// Handle pack
|
||||
let packOk = false, imgsOk = false;
|
||||
|
||||
try {
|
||||
const pack = await packP;
|
||||
if (pack.error) throw new Error(pack.error);
|
||||
setStep('#a-s2', 'done', `✓ 12 assets generated (${pack._generation_time})`);
|
||||
renderAssetPack(pack);
|
||||
$('#demoA-output').classList.remove('hidden');
|
||||
demoA_pack = await packP;
|
||||
if (demoA_pack.error) throw new Error(demoA_pack.error);
|
||||
packOk = true;
|
||||
setStep('#a-s2', 'done', `✓ PDP + assets generated (${demoA_pack._generation_time})`);
|
||||
} catch(e) {
|
||||
setStep('#a-s2', 'error', `✗ ${esc(e.message)}`);
|
||||
}
|
||||
|
||||
// Handle images
|
||||
try {
|
||||
const imgs = await imgP;
|
||||
if (imgs.error) throw new Error(imgs.error);
|
||||
setStep('#a-s3', 'done', `✓ Images generated (${imgs._generation_time})`);
|
||||
renderImages(imgs);
|
||||
demoA_imgs = await imgP;
|
||||
if (demoA_imgs.error && !demoA_imgs.original) throw new Error(demoA_imgs.error);
|
||||
imgsOk = true;
|
||||
setStep('#a-s3', 'done', `✓ Product images generated (${demoA_imgs._generation_time || ''})`);
|
||||
} catch(e) {
|
||||
setStep('#a-s3', 'error', `✗ ${esc(e.message)}`);
|
||||
}
|
||||
|
||||
if (packOk) {
|
||||
renderPDP(demoA_product, demoA_pack, demoA_imgs);
|
||||
$('#demoA-output').classList.remove('hidden');
|
||||
}
|
||||
|
||||
setBtn('#demoA-btn', false, '✓ Done — Run Again');
|
||||
}
|
||||
|
||||
function renderAssetPack(pack) {
|
||||
const meta = $('#demoA-meta');
|
||||
meta.innerHTML = `
|
||||
<span class="chip">🧠 <strong>Model:</strong> Gemini 2.5 Flash</span>
|
||||
<span class="chip">⏱ <strong>Time:</strong> ${esc(pack._generation_time)}</span>
|
||||
<span class="chip">📦 <strong>Product:</strong> ${esc(pack._product_title)}</span>
|
||||
`;
|
||||
|
||||
const grid = $('#demoA-assets');
|
||||
let cards = '';
|
||||
let n = 0;
|
||||
// ── Render as a real PDP page ───────────────────────────────
|
||||
|
||||
// Hero angles
|
||||
(pack.hero_angles || []).forEach((a, i) => {
|
||||
n++;
|
||||
cards += assetCard('hero', `Hero Angle`, n, `<strong>"${esc(a.headline)}"</strong><ul><li>Target: ${esc(a.target_desire)}</li><li>Best for: ${esc(a.best_for)}</li></ul>`, i*80);
|
||||
});
|
||||
function renderPDP(product, pack, imgs) {
|
||||
const pdp = pack.pdp || {};
|
||||
const out = $('#demoA-pdp');
|
||||
if (!out) return;
|
||||
|
||||
// PDP Copy
|
||||
if (pack.pdp_copy) {
|
||||
n++;
|
||||
const bullets = (pack.pdp_copy.bullets||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
||||
cards += assetCard('pdp', 'PDP Copy', n, `<strong>${esc(pack.pdp_copy.headline)}</strong><ul>${bullets}</ul>`, n*80);
|
||||
n++;
|
||||
const faqs = (pack.pdp_copy.faq||[]).map(f => `<strong>Q: ${esc(f.q)}</strong><br>A: ${esc(f.a)}<br><br>`).join('');
|
||||
cards += assetCard('pdp', 'FAQ Block', n, faqs, n*80);
|
||||
}
|
||||
// Build gallery images
|
||||
const gallery = buildGallery(product, imgs);
|
||||
const bullets = (pdp.benefit_bullets || []).map(b =>
|
||||
`<div class="pdp-bullet">
|
||||
<span class="pdp-bullet-icon">${esc(b.icon||'✓')}</span>
|
||||
<div><strong>${esc(b.headline)}</strong><br>
|
||||
<span class="pdp-bullet-detail">${esc(b.detail)}</span>
|
||||
${b.proof ? `<span class="pdp-bullet-proof">${esc(b.proof)}</span>` : ''}
|
||||
</div>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
// Ad hooks
|
||||
if (pack.ad_hooks) {
|
||||
n++;
|
||||
const hooks = pack.ad_hooks.map(h => `<li><strong>${esc(h)}</strong></li>`).join('');
|
||||
cards += assetCard('ad', '5 Ad Hooks', n, `<ul>${hooks}</ul>`, n*80);
|
||||
}
|
||||
const trustBar = (pdp.trust_signals || []).map(t =>
|
||||
`<div class="pdp-trust-item"><span class="pdp-trust-icon">${esc(t.icon)}</span><span>${esc(t.text)}</span></div>`
|
||||
).join('');
|
||||
|
||||
const whyParas = (pdp.why_paragraphs || []).map(p => `<p>${esc(p)}</p>`).join('');
|
||||
|
||||
const statsBar = (pdp.stats_bar || []).map(s =>
|
||||
`<div class="pdp-stat"><span class="pdp-stat-num">${esc(s.number)}</span><span class="pdp-stat-label">${esc(s.label)}</span></div>`
|
||||
).join('');
|
||||
|
||||
const faq = (pdp.faq || []).map((f,i) =>
|
||||
`<details class="pdp-faq-item" ${i===0?'open':''}>
|
||||
<summary>${esc(f.q)}</summary>
|
||||
<p>${esc(f.a)}</p>
|
||||
</details>`
|
||||
).join('');
|
||||
|
||||
const priceInfo = pdp.price_display || {};
|
||||
const review = pdp.review_quote || {};
|
||||
|
||||
// Ad hooks section
|
||||
const hooks = (pack.ad_hooks || []).map(h =>
|
||||
`<div class="pdp-hook">
|
||||
<div class="pdp-hook-text">"${esc(h.hook || h)}"</div>
|
||||
${h.angle ? `<span class="pdp-hook-meta">${esc(h.angle)} · ${esc(h.platform||'')}</span>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
// Email subjects
|
||||
if (pack.email_subjects) {
|
||||
n++;
|
||||
const emails = pack.email_subjects.map(e => `<strong>${esc(e.subject)}</strong><br><em style="color:var(--muted)">${esc(e.preview)}</em><br><br>`).join('');
|
||||
cards += assetCard('email', 'Email Subjects', n, emails, n*80);
|
||||
}
|
||||
|
||||
// TikTok
|
||||
if (pack.tiktok_script) {
|
||||
n++;
|
||||
const t = pack.tiktok_script;
|
||||
cards += assetCard('video', 'TikTok Script', n, `
|
||||
<strong>${esc(t.title)}</strong><br><br>
|
||||
<em>[0-3s]</em> ${esc(t.hook_0_3s)}<br>
|
||||
<em>[3-12s]</em> ${esc(t.body_3_12s)}<br>
|
||||
<em>[12-15s]</em> ${esc(t.cta_12_15s)}<br><br>
|
||||
<span class="ann">${esc(t.why_it_works)}</span>
|
||||
`, n*80);
|
||||
}
|
||||
|
||||
// Blog
|
||||
if (pack.blog_outline) {
|
||||
n++;
|
||||
const b = pack.blog_outline;
|
||||
const secs = (b.sections||[]).map(s => `<li>${esc(s)}</li>`).join('');
|
||||
cards += assetCard('blog', 'Blog Outline', n, `
|
||||
<strong>${esc(b.title)}</strong><ul>${secs}</ul>
|
||||
<span class="ann">SEO: "${esc(b.seo_keyword)}" — ${esc(b.monthly_searches)} mo/searches</span>
|
||||
`, n*80);
|
||||
}
|
||||
const emails = (pack.email_subjects || []).map(e =>
|
||||
`<div class="pdp-email">
|
||||
<span class="pdp-email-flow">${esc(e.flow||'')}</span>
|
||||
<strong>${esc(e.subject)}</strong>
|
||||
<span class="pdp-email-preview">${esc(e.preview)}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
// Meta SEO
|
||||
if (pack.meta_seo) {
|
||||
n++;
|
||||
const m = pack.meta_seo;
|
||||
cards += assetCard('seo', 'Meta SEO', n, `
|
||||
<strong>Title:</strong> ${esc(m.title)}<br><br>
|
||||
<strong>Description:</strong> ${esc(m.description)}<br><br>
|
||||
<span class="ann">${m.title_chars || '?'} chars title / ${m.desc_chars || '?'} chars desc</span>
|
||||
`, n*80);
|
||||
}
|
||||
const meta = pack.meta_seo || {};
|
||||
|
||||
// Alt text
|
||||
if (pack.alt_text) {
|
||||
n++;
|
||||
const alts = pack.alt_text.map(a => `<strong>${esc(a.image_type)}:</strong><br>Alt: ${esc(a.alt)}<br>File: <code style="color:var(--accent);font-size:.78rem">${esc(a.filename)}</code><br><br>`).join('');
|
||||
cards += assetCard('a11y', 'Alt Text + Filenames', n, alts, n*80);
|
||||
}
|
||||
out.innerHTML = `
|
||||
<!-- ═══ PDP LAYOUT ═══ -->
|
||||
<div class="pdp-page">
|
||||
|
||||
// A/B Variants
|
||||
if (pack.ab_variants) {
|
||||
n++;
|
||||
const vars = pack.ab_variants.map(v => `<strong>${esc(v.label)}:</strong> ${esc(v.copy)}<br><br>`).join('');
|
||||
cards += assetCard('ad', 'A/B Variants', n, vars + '<span class="ann">Test all — let data pick the winner</span>', n*80);
|
||||
}
|
||||
<!-- ABOVE THE FOLD -->
|
||||
<div class="pdp-atf">
|
||||
<div class="pdp-gallery">${gallery}</div>
|
||||
<div class="pdp-info">
|
||||
<div class="pdp-breadcrumb">${esc(product.category)} › ${esc(product.title.split(' ').slice(0,4).join(' '))}...</div>
|
||||
<h2 class="pdp-title">${esc(pdp.hero_headline || product.title)}</h2>
|
||||
<p class="pdp-subhead">${esc(pdp.hero_subhead || product.subtitle)}</p>
|
||||
|
||||
grid.innerHTML = cards;
|
||||
<div class="pdp-value-props">
|
||||
${(pdp.value_props||[]).map(v=>`<span class="pdp-vp">✓ ${esc(v)}</span>`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="pdp-price-block">
|
||||
<span class="pdp-price-main">${esc(priceInfo.main_price || product.price)}</span>
|
||||
<span class="pdp-price-qty">${esc(product.quantity)}</span>
|
||||
<div class="pdp-price-daily">${esc(priceInfo.price_per_day || '')}</div>
|
||||
<div class="pdp-price-compare">${esc(priceInfo.comparison || '')}</div>
|
||||
</div>
|
||||
|
||||
<div class="pdp-trust-bar">${trustBar}</div>
|
||||
|
||||
<button class="pdp-cta-primary">${esc(pdp.cta_primary || 'Add to Basket')}</button>
|
||||
<button class="pdp-cta-secondary">${esc(pdp.cta_secondary || 'Subscribe & Save 15%')}</button>
|
||||
|
||||
${pdp.urgency_note ? `<div class="pdp-urgency">⚡ ${esc(pdp.urgency_note)}</div>` : ''}
|
||||
<div class="pdp-usage">${esc(pdp.usage_instructions || '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BENEFIT BULLETS -->
|
||||
<div class="pdp-section">
|
||||
<h3>Key Benefits</h3>
|
||||
<div class="pdp-bullets">${bullets}</div>
|
||||
</div>
|
||||
|
||||
<!-- STATS BAR -->
|
||||
<div class="pdp-stats-bar">${statsBar}</div>
|
||||
|
||||
<!-- WHY THIS FORMULA -->
|
||||
<div class="pdp-section pdp-why">
|
||||
<h3>${esc(pdp.why_section_title || 'Why This Formula')}</h3>
|
||||
${whyParas}
|
||||
</div>
|
||||
|
||||
<!-- SOCIAL PROOF -->
|
||||
<div class="pdp-review-section">
|
||||
<div class="pdp-review-card">
|
||||
<div class="pdp-review-stars">${'★'.repeat(review.stars||5)}</div>
|
||||
<p class="pdp-review-text">"${esc(review.text || '')}"</p>
|
||||
<span class="pdp-review-author">— ${esc(review.author || 'Verified Buyer')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ -->
|
||||
<div class="pdp-section">
|
||||
<h3>Frequently Asked Questions</h3>
|
||||
<div class="pdp-faq">${faq}</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ ADDITIONAL ASSETS ═══ -->
|
||||
<div class="pdp-divider">
|
||||
<span>📦 Additional Generated Assets</span>
|
||||
</div>
|
||||
|
||||
<!-- META SEO -->
|
||||
${meta.title ? `
|
||||
<div class="pdp-asset-section">
|
||||
<h4>🔍 Meta SEO</h4>
|
||||
<div class="pdp-seo">
|
||||
<div class="pdp-seo-title">${esc(meta.title)}</div>
|
||||
<div class="pdp-seo-url">justvitamins.co.uk › ${esc(meta.primary_keyword || '')}</div>
|
||||
<div class="pdp-seo-desc">${esc(meta.description)}</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<!-- AD HOOKS -->
|
||||
${hooks ? `
|
||||
<div class="pdp-asset-section">
|
||||
<h4>📢 Ad Hooks</h4>
|
||||
<div class="pdp-hooks-grid">${hooks}</div>
|
||||
</div>` : ''}
|
||||
|
||||
<!-- EMAIL SUBJECTS -->
|
||||
${emails ? `
|
||||
<div class="pdp-asset-section">
|
||||
<h4>📧 Email Sequences</h4>
|
||||
<div class="pdp-emails">${emails}</div>
|
||||
</div>` : ''}
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function assetCard(type, label, num, content, delay) {
|
||||
return `<div class="asset-card" style="animation-delay:${delay}ms">
|
||||
<div class="asset-card-head"><span class="asset-type ${type}">${label}</span><span class="asset-num">#${num}</span></div>
|
||||
<div class="asset-body">${content}</div></div>`;
|
||||
}
|
||||
|
||||
function renderImages(imgs) {
|
||||
const grid = $('#demoA-img-grid');
|
||||
const section = $('#demoA-images');
|
||||
let html = '';
|
||||
const styles = [
|
||||
{key:'hero', label:'Hero Banner', desc:'Nano Banana Pro', wide:true},
|
||||
{key:'lifestyle', label:'Lifestyle Shot', desc:'Nano Banana'},
|
||||
{key:'benefits', label:'Benefits Visual', desc:'Nano Banana Pro'},
|
||||
];
|
||||
styles.forEach(s => {
|
||||
const data = imgs[s.key];
|
||||
if (data && data.filename) {
|
||||
html += `<div class="img-card ${s.wide?'wide':''}">
|
||||
<img src="/generated/${data.filename}" alt="${s.label}" loading="lazy">
|
||||
<div class="caption"><strong>${s.label}</strong> — ${s.desc} · ${data.model||''}</div></div>`;
|
||||
}
|
||||
function buildGallery(product, imgs) {
|
||||
const items = [];
|
||||
// Original product images first
|
||||
(product.images || []).forEach((src, i) => {
|
||||
items.push({src: src, label: i === 0 ? 'Original — Main' : `Original — ${i+1}`, isOriginal: true});
|
||||
});
|
||||
if (html) {
|
||||
grid.innerHTML = html;
|
||||
section.classList.remove('hidden');
|
||||
// AI-generated images
|
||||
if (imgs) {
|
||||
const aiSlots = [
|
||||
{key:'hero', label:'AI — Clean Studio'},
|
||||
{key:'lifestyle', label:'AI — Lifestyle'},
|
||||
{key:'scale', label:'AI — Scale'},
|
||||
{key:'ingredients', label:'AI — Detail'},
|
||||
{key:'banner', label:'AI — Banner', wide: true},
|
||||
];
|
||||
aiSlots.forEach(slot => {
|
||||
const d = imgs[slot.key];
|
||||
if (d && d.filename) {
|
||||
items.push({src: `/generated/${d.filename}`, label: slot.label,
|
||||
caption: d.caption || '', model: d.model || '', wide: slot.wide});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!items.length) return '<div class="pdp-gallery-empty">No images available</div>';
|
||||
|
||||
const mainImg = items[0];
|
||||
const thumbs = items.map((item, i) =>
|
||||
`<div class="pdp-thumb ${i===0?'active':''} ${item.wide?'wide':''}" onclick="switchGallery(${i}, this)" data-src="${esc(item.src)}">
|
||||
<img src="${esc(item.src)}" alt="${esc(item.label)}" loading="lazy">
|
||||
<span class="pdp-thumb-label ${item.isOriginal?'orig':'ai'}">${esc(item.label)}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="pdp-gallery-main" id="pdpGalleryMain">
|
||||
<img src="${esc(mainImg.src)}" alt="${esc(mainImg.label)}" id="pdpMainImg">
|
||||
<span class="pdp-gallery-badge">${mainImg.isOriginal ? '📷 Original' : '🤖 AI Generated'}</span>
|
||||
</div>
|
||||
<div class="pdp-thumbs">${thumbs}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.switchGallery = function(idx, el) {
|
||||
const src = el.dataset.src;
|
||||
const mainImg = $('#pdpMainImg');
|
||||
if (mainImg) mainImg.src = src;
|
||||
document.querySelectorAll('.pdp-thumb').forEach(t => t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
const badge = $('.pdp-gallery-badge');
|
||||
if (badge) {
|
||||
const label = el.querySelector('.pdp-thumb-label');
|
||||
badge.textContent = label?.classList.contains('orig') ? '📷 Original' : '🤖 AI Generated';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DEMO B — Competitor X-Ray
|
||||
// DEMO B — Competitor X-Ray (unchanged logic, same render)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async function runDemoB() {
|
||||
const url = $('#demoB-url').value.trim();
|
||||
if (!url) return;
|
||||
|
||||
setBtn('#demoB-btn', true, 'Scanning...');
|
||||
$('#demoB-output').classList.add('hidden');
|
||||
setStep('#b-s1', 'active', '<span class="spinner"></span> Scraping competitor...');
|
||||
setStep('#b-s1', 'active', 'Scraping competitor...');
|
||||
setStep('#b-s2', '', 'Waiting');
|
||||
|
||||
try {
|
||||
@@ -229,10 +326,8 @@ async function runDemoB() {
|
||||
});
|
||||
const data = await r.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
setStep('#b-s1', 'done', `✓ Scraped: ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
||||
setStep('#b-s2', 'done', `✓ Analysis complete (${data._generation_time})`);
|
||||
|
||||
setStep('#b-s1', 'done', `✓ ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
||||
setStep('#b-s2', 'done', `✓ Analysis (${data._generation_time})`);
|
||||
renderXray(data);
|
||||
$('#demoB-output').classList.remove('hidden');
|
||||
setBtn('#demoB-btn', false, '✓ Done — Try Another');
|
||||
@@ -243,11 +338,8 @@ async function runDemoB() {
|
||||
}
|
||||
|
||||
function renderXray(data) {
|
||||
// Left — competitor analysis
|
||||
const tactics = (data.top_5_tactics||[]).map(t =>
|
||||
`<li><strong>${esc(t.tactic)}</strong> — ${esc(t.explanation)}</li>`
|
||||
).join('');
|
||||
|
||||
`<li><strong>${esc(t.tactic)}</strong> — ${esc(t.explanation)}</li>`).join('');
|
||||
$('#demoB-left').innerHTML = `
|
||||
<span class="split-label bad">❌ ${esc(data.competitor_name || 'Competitor')}</span>
|
||||
<div class="xray-item"><div class="xray-label">What they're really selling</div>
|
||||
@@ -255,52 +347,37 @@ function renderXray(data) {
|
||||
<div class="xray-item"><div class="xray-label">Top 5 Persuasion Tactics</div>
|
||||
<ol class="xray-tactics">${tactics}</ol></div>
|
||||
<div class="xray-item"><div class="xray-label">Weakest Claim / Gap</div>
|
||||
<div class="xray-val gap">⚠️ ${esc(data.weakest_claim)}</div></div>
|
||||
`;
|
||||
<div class="xray-val gap">⚠️ ${esc(data.weakest_claim)}</div></div>`;
|
||||
|
||||
// Right — JV improved
|
||||
const hero = data.jv_hero_section || {};
|
||||
const diffs = (data.differentiators||[]).map(d =>
|
||||
`<li><span class="icon">🎯</span><span class="txt"><strong>${esc(d.point)}</strong> — ${esc(d.proof_idea)}</span></li>`
|
||||
).join('');
|
||||
`<li><span class="icon">🎯</span><span class="txt"><strong>${esc(d.point)}</strong> — ${esc(d.proof_idea)}</span></li>`).join('');
|
||||
const donts = (data.do_not_say||[]).map(d => `<li>${esc(d)}</li>`).join('');
|
||||
|
||||
$('#demoB-right').innerHTML = `
|
||||
<span class="split-label good">✓ Just Vitamins — Upgraded</span>
|
||||
<div class="improved-hero">
|
||||
<h4>${esc(hero.headline)}</h4>
|
||||
<p>${esc(hero.body)}</p>
|
||||
<p style="margin-top:.5rem;color:var(--accent);font-weight:600">${esc(hero.value_prop)}</p>
|
||||
</div>
|
||||
<div class="xray-item"><div class="xray-label">3 Differentiators + Proof Ideas</div>
|
||||
<ul class="diff-list">${diffs}</ul></div>
|
||||
<div class="compliance"><h5>⚠️ Do Not Say — Compliance</h5><ul>${donts}</ul></div>
|
||||
`;
|
||||
<div class="improved-hero"><h4>${esc(hero.headline)}</h4><p>${esc(hero.body)}</p>
|
||||
<p style="margin-top:.5rem;color:var(--accent);font-weight:600">${esc(hero.value_prop)}</p></div>
|
||||
<div class="xray-item"><div class="xray-label">3 Differentiators</div><ul class="diff-list">${diffs}</ul></div>
|
||||
<div class="compliance"><h5>⚠️ Do Not Say</h5><ul>${donts}</ul></div>`;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DEMO C — PDP Surgeon
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
let demoC_product = null;
|
||||
let demoC_cache = {};
|
||||
let demoC_product = null, demoC_cache = {};
|
||||
|
||||
async function runDemoC() {
|
||||
const url = $('#demoC-url').value.trim();
|
||||
if (!url) return;
|
||||
|
||||
setBtn('#demoC-btn', true, 'Working...');
|
||||
$('#demoC-output').classList.add('hidden');
|
||||
demoC_cache = {};
|
||||
setStep('#c-s1', 'active', '<span class="spinner"></span> Scraping product...');
|
||||
setStep('#c-s1', 'active', 'Scraping product...');
|
||||
setStep('#c-s2', '', 'Waiting');
|
||||
|
||||
// Step 1: Scrape
|
||||
try {
|
||||
const r = await fetch('/api/scrape', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({url})
|
||||
});
|
||||
const r = await fetch('/api/scrape', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
|
||||
const product = await r.json();
|
||||
if (product.error) throw new Error(product.error);
|
||||
demoC_product = product;
|
||||
@@ -310,12 +387,8 @@ async function runDemoC() {
|
||||
setBtn('#demoC-btn', false, '🎨 Scrape & Rewrite');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: AI rewrite (default style)
|
||||
const active = document.querySelector('#demoC-toggles .toggle.active');
|
||||
const style = active?.dataset.style || 'balanced';
|
||||
await rewriteStyle(style);
|
||||
|
||||
await rewriteStyle(active?.dataset.style || 'balanced');
|
||||
setBtn('#demoC-btn', false, '✓ Done — Change URL');
|
||||
}
|
||||
|
||||
@@ -327,73 +400,52 @@ async function switchDemoC(style, btn) {
|
||||
}
|
||||
|
||||
async function rewriteStyle(style) {
|
||||
if (demoC_cache[style]) {
|
||||
renderSurgeon(demoC_product, demoC_cache[style]);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep('#c-s2', 'active', `<span class="spinner"></span> Gemini rewriting as ${style}...`);
|
||||
|
||||
if (demoC_cache[style]) { renderSurgeon(demoC_product, demoC_cache[style]); return; }
|
||||
setStep('#c-s2', 'active', `Gemini rewriting as ${style}...`);
|
||||
try {
|
||||
const r = await fetch('/api/pdp-surgeon', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({product: demoC_product, style})
|
||||
});
|
||||
const r = await fetch('/api/pdp-surgeon', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({product:demoC_product,style})});
|
||||
const result = await r.json();
|
||||
if (result.error) throw new Error(result.error);
|
||||
demoC_cache[style] = result;
|
||||
setStep('#c-s2', 'done', `✓ ${style} rewrite (${result._generation_time})`);
|
||||
setStep('#c-s2', 'done', `✓ ${style} (${result._generation_time})`);
|
||||
renderSurgeon(demoC_product, result);
|
||||
$('#demoC-output').classList.remove('hidden');
|
||||
} catch(e) {
|
||||
setStep('#c-s2', 'error', `✗ ${esc(e.message)}`);
|
||||
}
|
||||
} catch(e) { setStep('#c-s2', 'error', `✗ ${esc(e.message)}`); }
|
||||
}
|
||||
|
||||
function renderSurgeon(product, result) {
|
||||
// Left — current
|
||||
const bullets = (product.benefits||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
||||
$('#demoC-left').innerHTML = `
|
||||
<span class="split-label bad">✕ Current PDP</span>
|
||||
${product.images?.[0] ? `<img src="${esc(product.images[0])}" style="width:100%;border-radius:8px;margin-bottom:.75rem" alt="Current">` : ''}
|
||||
<h4 style="font-size:1rem;margin-bottom:.5rem">${esc(product.title)}</h4>
|
||||
<p style="font-size:.84rem;color:var(--muted);margin-bottom:.75rem">${esc(product.subtitle)}</p>
|
||||
<p style="font-size:1.2rem;font-weight:800;color:var(--accent);margin-bottom:.5rem">${esc(product.price)} <span style="font-size:.78rem;color:var(--muted);font-weight:400">${esc(product.quantity)}</span></p>
|
||||
<ul style="list-style:none;margin-bottom:1rem">${bullets.replace(/<li>/g, '<li style="padding:.3rem 0;font-size:.84rem;color:var(--text2)">')}</ul>
|
||||
<p style="font-size:.84rem;color:var(--muted);line-height:1.6">${esc((product.description||'').substring(0,400))}...</p>
|
||||
`;
|
||||
<p style="font-size:.84rem;color:var(--muted);margin-bottom:.5rem">${esc(product.subtitle)}</p>
|
||||
<p style="font-size:1.2rem;font-weight:800;color:var(--accent);margin-bottom:.5rem">${esc(product.price)} <span style="font-size:.78rem;color:var(--muted)">${esc(product.quantity)}</span></p>
|
||||
<ul style="list-style:none">${bullets.replace(/<li>/g,'<li style="padding:.2rem 0;font-size:.82rem;color:var(--text2)">')}</ul>`;
|
||||
|
||||
// Right — rewritten
|
||||
const rBullets = (result.bullets||[]).map(b =>
|
||||
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`
|
||||
).join('');
|
||||
|
||||
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`).join('');
|
||||
$('#demoC-right').innerHTML = `
|
||||
<span class="split-label good">✓ AI-Rewritten — ${esc(result.style||'balanced').toUpperCase()}</span>
|
||||
<span class="split-label good">✓ ${esc(result.style||'balanced').toUpperCase()}</span>
|
||||
<h4 style="font-size:1rem;margin-bottom:.25rem">${esc(result.title)}</h4>
|
||||
<span class="ann">↑ SEO-optimised title</span>
|
||||
<p style="font-size:.84rem;color:var(--accent2);margin:.5rem 0">${esc(result.subtitle)}</p>
|
||||
|
||||
<span class="ann">↑ SEO title</span>
|
||||
<p style="font-size:.84rem;color:var(--accent2);margin:.4rem 0">${esc(result.subtitle)}</p>
|
||||
<div class="highlight" style="font-weight:600">${esc(result.hero_copy)}</div>
|
||||
<span class="ann">↑ ${esc(result.hero_annotation)}</span>
|
||||
|
||||
<div style="margin-top:.75rem">${rBullets}</div>
|
||||
|
||||
<div style="margin-top:.75rem;padding:.6rem .8rem;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);border-radius:8px;font-size:.84rem;color:var(--gold)">⭐ ${esc(result.social_proof)}</div>
|
||||
<div style="margin-top:.6rem">${rBullets}</div>
|
||||
<div style="margin-top:.6rem;padding:.5rem .7rem;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);border-radius:8px;font-size:.82rem;color:var(--gold)">⭐ ${esc(result.social_proof)}</div>
|
||||
<span class="ann">↑ ${esc(result.social_proof_annotation)}</span>
|
||||
|
||||
<div style="margin-top:.75rem;font-size:1.05rem;font-weight:800;color:var(--accent)">${esc(result.price_reframe)}</div>
|
||||
<div style="margin-top:.6rem;font-size:1rem;font-weight:800;color:var(--accent)">${esc(result.price_reframe)}</div>
|
||||
<span class="ann">↑ ${esc(result.price_annotation)}</span>
|
||||
|
||||
<p style="margin-top:.75rem;font-size:.84rem;color:var(--text2);font-style:italic">${esc(result.usage_instruction)}</p>
|
||||
<p style="margin-top:.5rem;font-size:.82rem;color:var(--text2);font-style:italic">${esc(result.usage_instruction)}</p>
|
||||
<span class="ann">↑ ${esc(result.usage_annotation)}</span>
|
||||
|
||||
<div style="margin-top:1rem;text-align:center">
|
||||
<button style="background:var(--accent);color:#060a0f;border:none;padding:.7rem 2rem;border-radius:8px;font-size:.9rem;font-weight:700;cursor:pointer">${esc(result.cta_text || 'Add to Basket')}</button>
|
||||
<div style="margin-top:.75rem;text-align:center">
|
||||
<button style="background:var(--accent);color:#060a0f;border:none;padding:.65rem 1.8rem;border-radius:8px;font-size:.88rem;font-weight:700;cursor:pointer">${esc(result.cta_text||'Add to Basket')}</button>
|
||||
</div>
|
||||
`;
|
||||
${result.cta_annotation ? `<span class="ann" style="display:block;text-align:center;margin-top:.3rem">↑ ${esc(result.cta_annotation)}</span>` : ''}`;
|
||||
}
|
||||
|
||||
// Smooth scroll nav
|
||||
// Smooth scroll
|
||||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', e => {
|
||||
const t = document.querySelector(a.getAttribute('href'));
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user