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:
441
ai_engine.py
441
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:
|
Image generation uses the REAL scraped product image as a reference.
|
||||||
Text: gemini-2.5-flash — all copy generation
|
Gemini receives the actual photo and generates ecommerce-grade variations
|
||||||
Image: gemini-2.5-flash-image — Nano Banana (fast lifestyle shots)
|
that maintain product consistency.
|
||||||
Image: gemini-3-pro-image-preview — Nano Banana Pro (premium hero/product shots)
|
|
||||||
|
|
||||||
Powers:
|
Models:
|
||||||
Demo A: generate_asset_pack() — 1 product → 12 marketing assets
|
Text: gemini-2.5-flash
|
||||||
Demo B: competitor_xray() — competitor URL → analysis + JV upgrade
|
Image: gemini-2.5-flash-image — Nano Banana (fast edits)
|
||||||
Demo C: pdp_surgeon() — existing copy → style variants
|
Image: gemini-3-pro-image-preview — Nano Banana Pro (premium product photography)
|
||||||
PDP: optimise_pdp_copy() — full PDP rewrite
|
|
||||||
Images: generate_all_images() — Nano Banana product imagery
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, json, hashlib, re
|
import os, json, hashlib, re, io, time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import requests as http_requests
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
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_FAST = "gemini-2.5-flash-image" # Nano Banana
|
||||||
IMG_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro
|
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:
|
def _call_gemini(prompt: str, temperature: float = 0.7) -> dict:
|
||||||
"""Call Gemini text model, return parsed JSON."""
|
"""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]}
|
return {"error": "Failed to parse AI response", "raw": response.text[:500]}
|
||||||
|
|
||||||
|
|
||||||
def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
|
def _download_image(url: str) -> tuple:
|
||||||
"""Generate image via Nano Banana. Returns (filename, mime_type) or ('','')."""
|
"""Download image, return (bytes, mime_type) or (None, None)."""
|
||||||
if not client:
|
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 ("", "")
|
return ("", "")
|
||||||
|
|
||||||
cache_key = hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()[:14]
|
cache_key = hashlib.md5(f"{model}:{prompt}:{hashlib.md5(ref_bytes).hexdigest()[:8]}".encode()).hexdigest()[:16]
|
||||||
# Check cache
|
for ext in ("png", "jpg", "jpeg", "webp"):
|
||||||
for ext in ("png", "jpg", "jpeg"):
|
|
||||||
cached = GEN_DIR / f"{cache_key}.{ext}"
|
cached = GEN_DIR / f"{cache_key}.{ext}"
|
||||||
if cached.exists():
|
if cached.exists():
|
||||||
return (cached.name, f"image/{ext}")
|
return (cached.name, f"image/{ext}")
|
||||||
@@ -65,7 +86,10 @@ def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
|
|||||||
try:
|
try:
|
||||||
response = client.models.generate_content(
|
response = client.models.generate_content(
|
||||||
model=model,
|
model=model,
|
||||||
contents=prompt,
|
contents=[
|
||||||
|
types.Part.from_bytes(data=ref_bytes, mime_type=ref_mime),
|
||||||
|
prompt,
|
||||||
|
],
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig(
|
||||||
response_modalities=["IMAGE", "TEXT"],
|
response_modalities=["IMAGE", "TEXT"],
|
||||||
),
|
),
|
||||||
@@ -82,74 +106,268 @@ def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
|
|||||||
return ("", "")
|
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:
|
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:
|
PRODUCT DATA:
|
||||||
- Title: {product.get('title','')}
|
- Title: {product.get('title','')}
|
||||||
- Subtitle: {product.get('subtitle','')}
|
- Subtitle: {product.get('subtitle','')}
|
||||||
- Price: {product.get('price','')} for {product.get('quantity','')}
|
- Price: {product.get('price','')} for {product.get('quantity','')}
|
||||||
- Per unit: {product.get('per_unit_cost','')}
|
- 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','')}
|
- 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:
|
Return JSON:
|
||||||
{{
|
{{
|
||||||
"hero_angles": [
|
"pdp": {{
|
||||||
{{"headline":"…","target_desire":"…","best_for":"…"}},
|
"hero_headline": "Conversion-optimised H1",
|
||||||
{{"headline":"…","target_desire":"…","best_for":"…"}},
|
"hero_subhead": "One sentence that makes them stay",
|
||||||
{{"headline":"…","target_desire":"…","best_for":"…"}}
|
"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": "…"}}
|
||||||
],
|
],
|
||||||
"pdp_copy": {{
|
"trust_signals": [
|
||||||
"headline":"…",
|
{{"icon": "⭐", "text": "4.8★ Trustpilot"}},
|
||||||
"bullets":["…","…","…","…","…"],
|
{{"icon": "🏆", "text": "20 Years Trusted"}},
|
||||||
"faq":[{{"q":"…","a":"…"}},{{"q":"…","a":"…"}},{{"q":"…","a":"…"}}]
|
{{"icon": "🇬🇧", "text": "UK Made · GMP"}},
|
||||||
}},
|
{{"icon": "🌿", "text": "Eco Bio-Pouch"}}
|
||||||
"ad_hooks":["…","…","…","…","…"],
|
|
||||||
"email_subjects":[
|
|
||||||
{{"subject":"…","preview":"…"}},
|
|
||||||
{{"subject":"…","preview":"…"}},
|
|
||||||
{{"subject":"…","preview":"…"}}
|
|
||||||
],
|
],
|
||||||
"tiktok_script":{{
|
"why_section_title": "Why This Formula",
|
||||||
"title":"…",
|
"why_paragraphs": ["Science paragraph 1", "Absorption/bioavailability paragraph 2", "Who benefits paragraph 3"],
|
||||||
"hook_0_3s":"…",
|
"review_quote": {{"text": "Realistic-sounding 5★ review", "author": "Verified Buyer name", "stars": 5}},
|
||||||
"body_3_12s":"…",
|
"stats_bar": [
|
||||||
"cta_12_15s":"…",
|
{{"number": "230,000+", "label": "Happy Customers"}},
|
||||||
"why_it_works":"…"
|
{{"number": "4.8★", "label": "Trustpilot Rating"}},
|
||||||
}},
|
{{"number": "20+", "label": "Years Trusted"}}
|
||||||
"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":[
|
"price_display": {{
|
||||||
{{"label":"Rational","copy":"…"}},
|
"main_price": "{product.get('price','')}",
|
||||||
{{"label":"Emotional","copy":"…"}},
|
"price_per_day": "Daily cost calculation",
|
||||||
{{"label":"Social Proof","copy":"…"}}
|
"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": "…"}}
|
||||||
|
],
|
||||||
|
"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)
|
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
|
# DEMO B — Competitor X-Ray
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
@@ -201,51 +419,58 @@ RULES: No false claims. EFSA/ASA compliant. Strategic, not aggressive."""
|
|||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
STYLE_INSTRUCTIONS = {
|
STYLE_INSTRUCTIONS = {
|
||||||
"balanced": "Balanced, trustworthy DTC supplement voice. Mix emotional hooks with rational proof.",
|
"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, target affluent buyers.",
|
"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, scarcity, stacked bonuses.",
|
"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, FDA disclaimer.",
|
"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:
|
def pdp_surgeon(product: dict, style: str = "balanced") -> dict:
|
||||||
instruction = STYLE_INSTRUCTIONS.get(style, STYLE_INSTRUCTIONS["balanced"])
|
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:
|
PRODUCT:
|
||||||
- Title: {product.get('title','')}
|
- Title: {product.get('title','')}
|
||||||
- Subtitle: {product.get('subtitle','')}
|
- Subtitle: {product.get('subtitle','')}
|
||||||
- Price: {product.get('price','')} for {product.get('quantity','')}
|
- Price: {product.get('price','')} for {product.get('quantity','')}
|
||||||
|
- Per unit: {product.get('per_unit_cost','')}
|
||||||
- Benefits: {json.dumps(product.get('benefits',[]))}
|
- Benefits: {json.dumps(product.get('benefits',[]))}
|
||||||
- Description: {product.get('description','')[:1500]}
|
- Description: {product.get('description','')[:1500]}
|
||||||
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
|
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
|
||||||
|
|
||||||
STYLE: {style.upper()} — {instruction}
|
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:
|
Output JSON:
|
||||||
{{
|
{{
|
||||||
"style":"{style}",
|
"style":"{style}",
|
||||||
"title":"…",
|
"title":"Conversion-optimised title",
|
||||||
"subtitle":"…",
|
"subtitle":"Desire-triggering subtitle",
|
||||||
"hero_copy":"Main persuasion paragraph",
|
"hero_copy":"2-3 sentence persuasion paragraph — the most important copy on the page",
|
||||||
"hero_annotation":"Why this works — conversion impact",
|
"hero_annotation":"Why this works — which conversion principle, estimated % lift",
|
||||||
"bullets":[
|
"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":"…"}},
|
{{"text":"…","annotation":"…"}},
|
||||||
{{"text":"…","annotation":"…"}}
|
{{"text":"…","annotation":"…"}}
|
||||||
],
|
],
|
||||||
"social_proof":"Line using 4.8★ Trustpilot, 230K customers",
|
"social_proof":"Specific social proof line using 4.8★ Trustpilot, 230K customers, years trading",
|
||||||
"social_proof_annotation":"…",
|
"social_proof_annotation":"Which social proof principle this uses + estimated lift",
|
||||||
"price_reframe":"Reframe price as no-brainer",
|
"price_reframe":"Reframe the price as a no-brainer — use daily cost, comparison anchoring",
|
||||||
"price_annotation":"…",
|
"price_annotation":"Price psychology principle + estimated lift",
|
||||||
"cta_text":"CTA button text",
|
"cta_text":"CTA button text — action-oriented, benefit-driven",
|
||||||
"usage_instruction":"How to take — written to remove friction",
|
"cta_annotation":"Why this CTA works",
|
||||||
"usage_annotation":"…"
|
"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)
|
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:
|
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:
|
PRODUCT:
|
||||||
- Title: {product['title']}
|
- Title: {product['title']}
|
||||||
- Subtitle: {product.get('subtitle','')}
|
- Subtitle: {product.get('subtitle','')}
|
||||||
- Price: {product.get('price','')} for {product.get('quantity','')}
|
- Price: {product.get('price','')} for {product.get('quantity','')}
|
||||||
- Per unit: {product.get('per_unit_cost','')}
|
|
||||||
- Benefits: {json.dumps(product.get('benefits',[]))}
|
- Benefits: {json.dumps(product.get('benefits',[]))}
|
||||||
- Description: {product.get('description','')[:1500]}
|
- Description: {product.get('description','')[:1500]}
|
||||||
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
|
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
|
||||||
- Category: {product.get('category','')}
|
|
||||||
|
|
||||||
Rewrite everything. Output JSON:
|
Rewrite everything. Output JSON:
|
||||||
{{
|
{{
|
||||||
@@ -278,51 +501,5 @@ Rewrite everything. Output JSON:
|
|||||||
"faqs":[{{"q":"…","a":"…"}},{{"q":"…","a":"…"}},{{"q":"…","a":"…"}}]
|
"faqs":[{{"q":"…","a":"…"}},{{"q":"…","a":"…"}},{{"q":"…","a":"…"}}]
|
||||||
}}
|
}}
|
||||||
|
|
||||||
Keep EFSA claims accurate. UK English."""
|
EFSA claims only. UK English."""
|
||||||
return _call_gemini(prompt, 0.7)
|
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{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}
|
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 */
|
/* 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: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}}
|
||||||
|
|||||||
462
static/js/app.js
462
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 $ = s => document.querySelector(s);
|
||||||
const esc = s => { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
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.parentElement.className = `step ${cls}`;
|
||||||
el.innerHTML = text;
|
el.innerHTML = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBtn(id, loading, text) {
|
function setBtn(id, loading, text) {
|
||||||
const btn = $(id);
|
const btn = $(id);
|
||||||
btn.disabled = loading;
|
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_product = null;
|
||||||
|
let demoA_pack = null;
|
||||||
|
let demoA_imgs = null;
|
||||||
|
|
||||||
async function runDemoA() {
|
async function runDemoA() {
|
||||||
const url = $('#demoA-url').value.trim();
|
const url = $('#demoA-url').value.trim();
|
||||||
@@ -32,7 +34,7 @@ async function runDemoA() {
|
|||||||
['#a-s1','#a-s2','#a-s3'].forEach(s => setStep(s, '', 'Waiting'));
|
['#a-s1','#a-s2','#a-s3'].forEach(s => setStep(s, '', 'Waiting'));
|
||||||
|
|
||||||
// Step 1: Scrape
|
// Step 1: Scrape
|
||||||
setStep('#a-s1', 'active', '<span class="spinner"></span> Scraping...');
|
setStep('#a-s1', 'active', 'Scraping product page...');
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/scrape', {
|
const r = await fetch('/api/scrape', {
|
||||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||||
@@ -41,16 +43,16 @@ async function runDemoA() {
|
|||||||
const product = await r.json();
|
const product = await r.json();
|
||||||
if (product.error) throw new Error(product.error);
|
if (product.error) throw new Error(product.error);
|
||||||
demoA_product = product;
|
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) {
|
} catch(e) {
|
||||||
setStep('#a-s1', 'error', `✗ ${esc(e.message)}`);
|
setStep('#a-s1', 'error', `✗ ${esc(e.message)}`);
|
||||||
setBtn('#demoA-btn', false, '🔴 Generate the Whole Pack');
|
setBtn('#demoA-btn', false, '🔴 Generate the Whole Pack');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: AI pack + Step 3: Images (parallel)
|
// Step 2 + 3: AI copy + images in parallel
|
||||||
setStep('#a-s2', 'active', '<span class="spinner"></span> Gemini generating 12 assets...');
|
setStep('#a-s2', 'active', 'Gemini generating PDP copy...');
|
||||||
setStep('#a-s3', 'active', '<span class="spinner"></span> Nano Banana generating images...');
|
setStep('#a-s3', 'active', 'Generating product images from real photo...');
|
||||||
|
|
||||||
const packP = fetch('/api/generate-pack', {
|
const packP = fetch('/api/generate-pack', {
|
||||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||||
@@ -62,164 +64,259 @@ async function runDemoA() {
|
|||||||
body: JSON.stringify(demoA_product)
|
body: JSON.stringify(demoA_product)
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
|
|
||||||
// Handle pack
|
let packOk = false, imgsOk = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pack = await packP;
|
demoA_pack = await packP;
|
||||||
if (pack.error) throw new Error(pack.error);
|
if (demoA_pack.error) throw new Error(demoA_pack.error);
|
||||||
setStep('#a-s2', 'done', `✓ 12 assets generated (${pack._generation_time})`);
|
packOk = true;
|
||||||
renderAssetPack(pack);
|
setStep('#a-s2', 'done', `✓ PDP + assets generated (${demoA_pack._generation_time})`);
|
||||||
$('#demoA-output').classList.remove('hidden');
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
setStep('#a-s2', 'error', `✗ ${esc(e.message)}`);
|
setStep('#a-s2', 'error', `✗ ${esc(e.message)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle images
|
|
||||||
try {
|
try {
|
||||||
const imgs = await imgP;
|
demoA_imgs = await imgP;
|
||||||
if (imgs.error) throw new Error(imgs.error);
|
if (demoA_imgs.error && !demoA_imgs.original) throw new Error(demoA_imgs.error);
|
||||||
setStep('#a-s3', 'done', `✓ Images generated (${imgs._generation_time})`);
|
imgsOk = true;
|
||||||
renderImages(imgs);
|
setStep('#a-s3', 'done', `✓ Product images generated (${demoA_imgs._generation_time || ''})`);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
setStep('#a-s3', 'error', `✗ ${esc(e.message)}`);
|
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');
|
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');
|
// ── Render as a real PDP page ───────────────────────────────
|
||||||
let cards = '';
|
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
// Hero angles
|
function renderPDP(product, pack, imgs) {
|
||||||
(pack.hero_angles || []).forEach((a, i) => {
|
const pdp = pack.pdp || {};
|
||||||
n++;
|
const out = $('#demoA-pdp');
|
||||||
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);
|
if (!out) return;
|
||||||
});
|
|
||||||
|
|
||||||
// PDP Copy
|
// Build gallery images
|
||||||
if (pack.pdp_copy) {
|
const gallery = buildGallery(product, imgs);
|
||||||
n++;
|
const bullets = (pdp.benefit_bullets || []).map(b =>
|
||||||
const bullets = (pack.pdp_copy.bullets||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
`<div class="pdp-bullet">
|
||||||
cards += assetCard('pdp', 'PDP Copy', n, `<strong>${esc(pack.pdp_copy.headline)}</strong><ul>${bullets}</ul>`, n*80);
|
<span class="pdp-bullet-icon">${esc(b.icon||'✓')}</span>
|
||||||
n++;
|
<div><strong>${esc(b.headline)}</strong><br>
|
||||||
const faqs = (pack.pdp_copy.faq||[]).map(f => `<strong>Q: ${esc(f.q)}</strong><br>A: ${esc(f.a)}<br><br>`).join('');
|
<span class="pdp-bullet-detail">${esc(b.detail)}</span>
|
||||||
cards += assetCard('pdp', 'FAQ Block', n, faqs, n*80);
|
${b.proof ? `<span class="pdp-bullet-proof">${esc(b.proof)}</span>` : ''}
|
||||||
}
|
</div>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
// Ad hooks
|
const trustBar = (pdp.trust_signals || []).map(t =>
|
||||||
if (pack.ad_hooks) {
|
`<div class="pdp-trust-item"><span class="pdp-trust-icon">${esc(t.icon)}</span><span>${esc(t.text)}</span></div>`
|
||||||
n++;
|
).join('');
|
||||||
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 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
|
// Email subjects
|
||||||
if (pack.email_subjects) {
|
const emails = (pack.email_subjects || []).map(e =>
|
||||||
n++;
|
`<div class="pdp-email">
|
||||||
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('');
|
<span class="pdp-email-flow">${esc(e.flow||'')}</span>
|
||||||
cards += assetCard('email', 'Email Subjects', n, emails, n*80);
|
<strong>${esc(e.subject)}</strong>
|
||||||
}
|
<span class="pdp-email-preview">${esc(e.preview)}</span>
|
||||||
|
</div>`
|
||||||
// TikTok
|
).join('');
|
||||||
if (pack.tiktok_script) {
|
|
||||||
n++;
|
|
||||||
const t = pack.tiktok_script;
|
|
||||||
cards += assetCard('video', 'TikTok Script', n, `
|
|
||||||
<strong>${esc(t.title)}</strong><br><br>
|
|
||||||
<em>[0-3s]</em> ${esc(t.hook_0_3s)}<br>
|
|
||||||
<em>[3-12s]</em> ${esc(t.body_3_12s)}<br>
|
|
||||||
<em>[12-15s]</em> ${esc(t.cta_12_15s)}<br><br>
|
|
||||||
<span class="ann">${esc(t.why_it_works)}</span>
|
|
||||||
`, n*80);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blog
|
|
||||||
if (pack.blog_outline) {
|
|
||||||
n++;
|
|
||||||
const b = pack.blog_outline;
|
|
||||||
const secs = (b.sections||[]).map(s => `<li>${esc(s)}</li>`).join('');
|
|
||||||
cards += assetCard('blog', 'Blog Outline', n, `
|
|
||||||
<strong>${esc(b.title)}</strong><ul>${secs}</ul>
|
|
||||||
<span class="ann">SEO: "${esc(b.seo_keyword)}" — ${esc(b.monthly_searches)} mo/searches</span>
|
|
||||||
`, n*80);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meta SEO
|
// Meta SEO
|
||||||
if (pack.meta_seo) {
|
const meta = pack.meta_seo || {};
|
||||||
n++;
|
|
||||||
const m = pack.meta_seo;
|
|
||||||
cards += assetCard('seo', 'Meta SEO', n, `
|
|
||||||
<strong>Title:</strong> ${esc(m.title)}<br><br>
|
|
||||||
<strong>Description:</strong> ${esc(m.description)}<br><br>
|
|
||||||
<span class="ann">${m.title_chars || '?'} chars title / ${m.desc_chars || '?'} chars desc</span>
|
|
||||||
`, n*80);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alt text
|
out.innerHTML = `
|
||||||
if (pack.alt_text) {
|
<!-- ═══ PDP LAYOUT ═══ -->
|
||||||
n++;
|
<div class="pdp-page">
|
||||||
const alts = pack.alt_text.map(a => `<strong>${esc(a.image_type)}:</strong><br>Alt: ${esc(a.alt)}<br>File: <code style="color:var(--accent);font-size:.78rem">${esc(a.filename)}</code><br><br>`).join('');
|
|
||||||
cards += assetCard('a11y', 'Alt Text + Filenames', n, alts, n*80);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A/B Variants
|
<!-- ABOVE THE FOLD -->
|
||||||
if (pack.ab_variants) {
|
<div class="pdp-atf">
|
||||||
n++;
|
<div class="pdp-gallery">${gallery}</div>
|
||||||
const vars = pack.ab_variants.map(v => `<strong>${esc(v.label)}:</strong> ${esc(v.copy)}<br><br>`).join('');
|
<div class="pdp-info">
|
||||||
cards += assetCard('ad', 'A/B Variants', n, vars + '<span class="ann">Test all — let data pick the winner</span>', n*80);
|
<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) {
|
function buildGallery(product, imgs) {
|
||||||
const grid = $('#demoA-img-grid');
|
const items = [];
|
||||||
const section = $('#demoA-images');
|
// Original product images first
|
||||||
let html = '';
|
(product.images || []).forEach((src, i) => {
|
||||||
const styles = [
|
items.push({src: src, label: i === 0 ? 'Original — Main' : `Original — ${i+1}`, isOriginal: true});
|
||||||
{key:'hero', label:'Hero Banner', desc:'Nano Banana Pro', wide:true},
|
});
|
||||||
{key:'lifestyle', label:'Lifestyle Shot', desc:'Nano Banana'},
|
// AI-generated images
|
||||||
{key:'benefits', label:'Benefits Visual', desc:'Nano Banana Pro'},
|
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},
|
||||||
];
|
];
|
||||||
styles.forEach(s => {
|
aiSlots.forEach(slot => {
|
||||||
const data = imgs[s.key];
|
const d = imgs[slot.key];
|
||||||
if (data && data.filename) {
|
if (d && d.filename) {
|
||||||
html += `<div class="img-card ${s.wide?'wide':''}">
|
items.push({src: `/generated/${d.filename}`, label: slot.label,
|
||||||
<img src="/generated/${data.filename}" alt="${s.label}" loading="lazy">
|
caption: d.caption || '', model: d.model || '', wide: slot.wide});
|
||||||
<div class="caption"><strong>${s.label}</strong> — ${s.desc} · ${data.model||''}</div></div>`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (html) {
|
|
||||||
grid.innerHTML = html;
|
|
||||||
section.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function runDemoB() {
|
||||||
const url = $('#demoB-url').value.trim();
|
const url = $('#demoB-url').value.trim();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
setBtn('#demoB-btn', true, 'Scanning...');
|
setBtn('#demoB-btn', true, 'Scanning...');
|
||||||
$('#demoB-output').classList.add('hidden');
|
$('#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');
|
setStep('#b-s2', '', 'Waiting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -229,10 +326,8 @@ async function runDemoB() {
|
|||||||
});
|
});
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
setStep('#b-s1', 'done', `✓ ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
||||||
setStep('#b-s1', 'done', `✓ Scraped: ${esc((data._scrape_data?.title||'').substring(0,30))}...`);
|
setStep('#b-s2', 'done', `✓ Analysis (${data._generation_time})`);
|
||||||
setStep('#b-s2', 'done', `✓ Analysis complete (${data._generation_time})`);
|
|
||||||
|
|
||||||
renderXray(data);
|
renderXray(data);
|
||||||
$('#demoB-output').classList.remove('hidden');
|
$('#demoB-output').classList.remove('hidden');
|
||||||
setBtn('#demoB-btn', false, '✓ Done — Try Another');
|
setBtn('#demoB-btn', false, '✓ Done — Try Another');
|
||||||
@@ -243,11 +338,8 @@ async function runDemoB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderXray(data) {
|
function renderXray(data) {
|
||||||
// Left — competitor analysis
|
|
||||||
const tactics = (data.top_5_tactics||[]).map(t =>
|
const tactics = (data.top_5_tactics||[]).map(t =>
|
||||||
`<li><strong>${esc(t.tactic)}</strong> — ${esc(t.explanation)}</li>`
|
`<li><strong>${esc(t.tactic)}</strong> — ${esc(t.explanation)}</li>`).join('');
|
||||||
).join('');
|
|
||||||
|
|
||||||
$('#demoB-left').innerHTML = `
|
$('#demoB-left').innerHTML = `
|
||||||
<span class="split-label bad">❌ ${esc(data.competitor_name || 'Competitor')}</span>
|
<span class="split-label bad">❌ ${esc(data.competitor_name || 'Competitor')}</span>
|
||||||
<div class="xray-item"><div class="xray-label">What they're really selling</div>
|
<div class="xray-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>
|
<div class="xray-item"><div class="xray-label">Top 5 Persuasion Tactics</div>
|
||||||
<ol class="xray-tactics">${tactics}</ol></div>
|
<ol class="xray-tactics">${tactics}</ol></div>
|
||||||
<div class="xray-item"><div class="xray-label">Weakest Claim / Gap</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 hero = data.jv_hero_section || {};
|
||||||
const diffs = (data.differentiators||[]).map(d =>
|
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>`
|
`<li><span class="icon">🎯</span><span class="txt"><strong>${esc(d.point)}</strong> — ${esc(d.proof_idea)}</span></li>`).join('');
|
||||||
).join('');
|
|
||||||
const donts = (data.do_not_say||[]).map(d => `<li>${esc(d)}</li>`).join('');
|
const donts = (data.do_not_say||[]).map(d => `<li>${esc(d)}</li>`).join('');
|
||||||
|
|
||||||
$('#demoB-right').innerHTML = `
|
$('#demoB-right').innerHTML = `
|
||||||
<span class="split-label good">✓ Just Vitamins — Upgraded</span>
|
<span class="split-label good">✓ Just Vitamins — Upgraded</span>
|
||||||
<div class="improved-hero">
|
<div class="improved-hero"><h4>${esc(hero.headline)}</h4><p>${esc(hero.body)}</p>
|
||||||
<h4>${esc(hero.headline)}</h4>
|
<p style="margin-top:.5rem;color:var(--accent);font-weight:600">${esc(hero.value_prop)}</p></div>
|
||||||
<p>${esc(hero.body)}</p>
|
<div class="xray-item"><div class="xray-label">3 Differentiators</div><ul class="diff-list">${diffs}</ul></div>
|
||||||
<p style="margin-top:.5rem;color:var(--accent);font-weight:600">${esc(hero.value_prop)}</p>
|
<div class="compliance"><h5>⚠️ Do Not Say</h5><ul>${donts}</ul></div>`;
|
||||||
</div>
|
|
||||||
<div class="xray-item"><div class="xray-label">3 Differentiators + Proof Ideas</div>
|
|
||||||
<ul class="diff-list">${diffs}</ul></div>
|
|
||||||
<div class="compliance"><h5>⚠️ Do Not Say — Compliance</h5><ul>${donts}</ul></div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// DEMO C — PDP Surgeon
|
// DEMO C — PDP Surgeon
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
let demoC_product = null;
|
let demoC_product = null, demoC_cache = {};
|
||||||
let demoC_cache = {};
|
|
||||||
|
|
||||||
async function runDemoC() {
|
async function runDemoC() {
|
||||||
const url = $('#demoC-url').value.trim();
|
const url = $('#demoC-url').value.trim();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
setBtn('#demoC-btn', true, 'Working...');
|
setBtn('#demoC-btn', true, 'Working...');
|
||||||
$('#demoC-output').classList.add('hidden');
|
$('#demoC-output').classList.add('hidden');
|
||||||
demoC_cache = {};
|
demoC_cache = {};
|
||||||
setStep('#c-s1', 'active', '<span class="spinner"></span> Scraping product...');
|
setStep('#c-s1', 'active', 'Scraping product...');
|
||||||
setStep('#c-s2', '', 'Waiting');
|
setStep('#c-s2', '', 'Waiting');
|
||||||
|
|
||||||
// Step 1: Scrape
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/scrape', {
|
const r = await fetch('/api/scrape', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
|
||||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({url})
|
|
||||||
});
|
|
||||||
const product = await r.json();
|
const product = await r.json();
|
||||||
if (product.error) throw new Error(product.error);
|
if (product.error) throw new Error(product.error);
|
||||||
demoC_product = product;
|
demoC_product = product;
|
||||||
@@ -310,12 +387,8 @@ async function runDemoC() {
|
|||||||
setBtn('#demoC-btn', false, '🎨 Scrape & Rewrite');
|
setBtn('#demoC-btn', false, '🎨 Scrape & Rewrite');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: AI rewrite (default style)
|
|
||||||
const active = document.querySelector('#demoC-toggles .toggle.active');
|
const active = document.querySelector('#demoC-toggles .toggle.active');
|
||||||
const style = active?.dataset.style || 'balanced';
|
await rewriteStyle(active?.dataset.style || 'balanced');
|
||||||
await rewriteStyle(style);
|
|
||||||
|
|
||||||
setBtn('#demoC-btn', false, '✓ Done — Change URL');
|
setBtn('#demoC-btn', false, '✓ Done — Change URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,73 +400,52 @@ async function switchDemoC(style, btn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function rewriteStyle(style) {
|
async function rewriteStyle(style) {
|
||||||
if (demoC_cache[style]) {
|
if (demoC_cache[style]) { renderSurgeon(demoC_product, demoC_cache[style]); return; }
|
||||||
renderSurgeon(demoC_product, demoC_cache[style]);
|
setStep('#c-s2', 'active', `Gemini rewriting as ${style}...`);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStep('#c-s2', 'active', `<span class="spinner"></span> Gemini rewriting as ${style}...`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/pdp-surgeon', {
|
const r = await fetch('/api/pdp-surgeon', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({product:demoC_product,style})});
|
||||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({product: demoC_product, style})
|
|
||||||
});
|
|
||||||
const result = await r.json();
|
const result = await r.json();
|
||||||
if (result.error) throw new Error(result.error);
|
if (result.error) throw new Error(result.error);
|
||||||
demoC_cache[style] = result;
|
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);
|
renderSurgeon(demoC_product, result);
|
||||||
$('#demoC-output').classList.remove('hidden');
|
$('#demoC-output').classList.remove('hidden');
|
||||||
} catch(e) {
|
} catch(e) { setStep('#c-s2', 'error', `✗ ${esc(e.message)}`); }
|
||||||
setStep('#c-s2', 'error', `✗ ${esc(e.message)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSurgeon(product, result) {
|
function renderSurgeon(product, result) {
|
||||||
// Left — current
|
|
||||||
const bullets = (product.benefits||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
const bullets = (product.benefits||[]).map(b => `<li>${esc(b)}</li>`).join('');
|
||||||
$('#demoC-left').innerHTML = `
|
$('#demoC-left').innerHTML = `
|
||||||
<span class="split-label bad">✕ Current PDP</span>
|
<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>
|
<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:.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);font-weight:400">${esc(product.quantity)}</span></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;margin-bottom:1rem">${bullets.replace(/<li>/g, '<li style="padding:.3rem 0;font-size:.84rem;color:var(--text2)">')}</ul>
|
<ul style="list-style:none">${bullets.replace(/<li>/g,'<li style="padding:.2rem 0;font-size:.82rem;color:var(--text2)">')}</ul>`;
|
||||||
<p style="font-size:.84rem;color:var(--muted);line-height:1.6">${esc((product.description||'').substring(0,400))}...</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Right — rewritten
|
|
||||||
const rBullets = (result.bullets||[]).map(b =>
|
const rBullets = (result.bullets||[]).map(b =>
|
||||||
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`
|
`<div class="highlight">${esc(b.text)}</div><span class="ann">↑ ${esc(b.annotation)}</span>`).join('');
|
||||||
).join('');
|
|
||||||
|
|
||||||
$('#demoC-right').innerHTML = `
|
$('#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>
|
<h4 style="font-size:1rem;margin-bottom:.25rem">${esc(result.title)}</h4>
|
||||||
<span class="ann">↑ SEO-optimised title</span>
|
<span class="ann">↑ SEO title</span>
|
||||||
<p style="font-size:.84rem;color:var(--accent2);margin:.5rem 0">${esc(result.subtitle)}</p>
|
<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>
|
<div class="highlight" style="font-weight:600">${esc(result.hero_copy)}</div>
|
||||||
<span class="ann">↑ ${esc(result.hero_annotation)}</span>
|
<span class="ann">↑ ${esc(result.hero_annotation)}</span>
|
||||||
|
<div style="margin-top:.6rem">${rBullets}</div>
|
||||||
<div style="margin-top:.75rem">${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>
|
||||||
|
|
||||||
<div style="margin-top:.75rem;padding:.6rem .8rem;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);border-radius:8px;font-size:.84rem;color:var(--gold)">⭐ ${esc(result.social_proof)}</div>
|
|
||||||
<span class="ann">↑ ${esc(result.social_proof_annotation)}</span>
|
<span class="ann">↑ ${esc(result.social_proof_annotation)}</span>
|
||||||
|
<div style="margin-top:.6rem;font-size:1rem;font-weight:800;color:var(--accent)">${esc(result.price_reframe)}</div>
|
||||||
<div style="margin-top:.75rem;font-size:1.05rem;font-weight:800;color:var(--accent)">${esc(result.price_reframe)}</div>
|
|
||||||
<span class="ann">↑ ${esc(result.price_annotation)}</span>
|
<span class="ann">↑ ${esc(result.price_annotation)}</span>
|
||||||
|
<p style="margin-top:.5rem;font-size:.82rem;color:var(--text2);font-style:italic">${esc(result.usage_instruction)}</p>
|
||||||
<p style="margin-top:.75rem;font-size:.84rem;color:var(--text2);font-style:italic">${esc(result.usage_instruction)}</p>
|
|
||||||
<span class="ann">↑ ${esc(result.usage_annotation)}</span>
|
<span class="ann">↑ ${esc(result.usage_annotation)}</span>
|
||||||
|
<div style="margin-top:.75rem;text-align:center">
|
||||||
<div style="margin-top:1rem;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>
|
||||||
<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>
|
</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 => {
|
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||||
a.addEventListener('click', e => {
|
a.addEventListener('click', e => {
|
||||||
const t = document.querySelector(a.getAttribute('href'));
|
const t = document.querySelector(a.getAttribute('href'));
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
<!-- ═══ DEMO A — 12 Assets ═══ -->
|
<!-- ═══ DEMO A — 12 Assets ═══ -->
|
||||||
<section class="demo" id="demo-a">
|
<section class="demo" id="demo-a">
|
||||||
<span class="badge red">⚡ DEMO A — LIVE</span>
|
<span class="badge red">⚡ DEMO A — LIVE</span>
|
||||||
<h2>One Product → 12 Assets in Seconds</h2>
|
<h2>One Product URL → Full Conversion PDP</h2>
|
||||||
<p class="sub">Paste a real Just Vitamins product URL. Gemini scrapes it, then generates a full marketing pack.</p>
|
<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-card">
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
@@ -56,23 +56,17 @@
|
|||||||
<label>PRODUCT URL</label>
|
<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">
|
<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>
|
</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>
|
||||||
<div class="step-bar" id="demoA-steps">
|
<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-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-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. Image Gen</span><span class="step-status" id="a-s3">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>
|
</div>
|
||||||
|
|
||||||
<div id="demoA-output" class="output hidden">
|
<div id="demoA-output" class="output hidden">
|
||||||
<div class="output-meta" id="demoA-meta"></div>
|
<div id="demoA-pdp"></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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user