From 7e58ab1970b39e56e99dcc8ba676bb9107c00d22 Mon Sep 17 00:00:00 2001 From: Omair Saleh Date: Mon, 2 Mar 2026 20:41:30 +0800 Subject: [PATCH] v4: Real product image generation + conversion PDP output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ai_engine.py | 445 ++++++++++++++++++++++++++++------------- static/css/style.css | 112 ++++++++++- static/js/app.js | 466 ++++++++++++++++++++++++------------------- templates/index.html | 18 +- 4 files changed, 687 insertions(+), 354 deletions(-) diff --git a/ai_engine.py b/ai_engine.py index 128059a..e883a8f 100644 --- a/ai_engine.py +++ b/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 diff --git a/static/css/style.css b/static/css/style.css index a8667b4..ef6c5cf 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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}} diff --git a/static/js/app.js b/static/js/app.js index bb444be..57e590e 100644 --- a/static/js/app.js +++ b/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', ' 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', ' Gemini generating 12 assets...'); - setStep('#a-s3', 'active', ' 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 = ` - 🧠 Model: Gemini 2.5 Flash - Time: ${esc(pack._generation_time)} - 📦 Product: ${esc(pack._product_title)} - `; - 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, `"${esc(a.headline)}"`, 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 => `
  • ${esc(b)}
  • `).join(''); - cards += assetCard('pdp', 'PDP Copy', n, `${esc(pack.pdp_copy.headline)}`, n*80); - n++; - const faqs = (pack.pdp_copy.faq||[]).map(f => `Q: ${esc(f.q)}
    A: ${esc(f.a)}

    `).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 => + `
    + ${esc(b.icon||'✓')} +
    ${esc(b.headline)}
    + ${esc(b.detail)} + ${b.proof ? `${esc(b.proof)}` : ''} +
    +
    ` + ).join(''); - // Ad hooks - if (pack.ad_hooks) { - n++; - const hooks = pack.ad_hooks.map(h => `
  • ${esc(h)}
  • `).join(''); - cards += assetCard('ad', '5 Ad Hooks', n, ``, n*80); - } + const trustBar = (pdp.trust_signals || []).map(t => + `
    ${esc(t.icon)}${esc(t.text)}
    ` + ).join(''); + + const whyParas = (pdp.why_paragraphs || []).map(p => `

    ${esc(p)}

    `).join(''); + + const statsBar = (pdp.stats_bar || []).map(s => + `
    ${esc(s.number)}${esc(s.label)}
    ` + ).join(''); + + const faq = (pdp.faq || []).map((f,i) => + `
    + ${esc(f.q)} +

    ${esc(f.a)}

    +
    ` + ).join(''); + + const priceInfo = pdp.price_display || {}; + const review = pdp.review_quote || {}; + + // Ad hooks section + const hooks = (pack.ad_hooks || []).map(h => + `
    +
    "${esc(h.hook || h)}"
    + ${h.angle ? `${esc(h.angle)} · ${esc(h.platform||'')}` : ''} +
    ` + ).join(''); // Email subjects - if (pack.email_subjects) { - n++; - const emails = pack.email_subjects.map(e => `${esc(e.subject)}
    ${esc(e.preview)}

    `).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, ` - ${esc(t.title)}

    - [0-3s] ${esc(t.hook_0_3s)}
    - [3-12s] ${esc(t.body_3_12s)}
    - [12-15s] ${esc(t.cta_12_15s)}

    - ${esc(t.why_it_works)} - `, n*80); - } - - // Blog - if (pack.blog_outline) { - n++; - const b = pack.blog_outline; - const secs = (b.sections||[]).map(s => `
  • ${esc(s)}
  • `).join(''); - cards += assetCard('blog', 'Blog Outline', n, ` - ${esc(b.title)} - SEO: "${esc(b.seo_keyword)}" — ${esc(b.monthly_searches)} mo/searches - `, n*80); - } + const emails = (pack.email_subjects || []).map(e => + `
    + ${esc(e.flow||'')} + ${esc(e.subject)} + ${esc(e.preview)} +
    ` + ).join(''); // Meta SEO - if (pack.meta_seo) { - n++; - const m = pack.meta_seo; - cards += assetCard('seo', 'Meta SEO', n, ` - Title: ${esc(m.title)}

    - Description: ${esc(m.description)}

    - ${m.title_chars || '?'} chars title / ${m.desc_chars || '?'} chars desc - `, n*80); - } + const meta = pack.meta_seo || {}; - // Alt text - if (pack.alt_text) { - n++; - const alts = pack.alt_text.map(a => `${esc(a.image_type)}:
    Alt: ${esc(a.alt)}
    File: ${esc(a.filename)}

    `).join(''); - cards += assetCard('a11y', 'Alt Text + Filenames', n, alts, n*80); - } + out.innerHTML = ` + +
    - // A/B Variants - if (pack.ab_variants) { - n++; - const vars = pack.ab_variants.map(v => `${esc(v.label)}: ${esc(v.copy)}

    `).join(''); - cards += assetCard('ad', 'A/B Variants', n, vars + 'Test all — let data pick the winner', n*80); - } + +
    + +
    +
    ${esc(product.category)} › ${esc(product.title.split(' ').slice(0,4).join(' '))}...
    +

    ${esc(pdp.hero_headline || product.title)}

    +

    ${esc(pdp.hero_subhead || product.subtitle)}

    - grid.innerHTML = cards; +
    + ${(pdp.value_props||[]).map(v=>`✓ ${esc(v)}`).join('')} +
    + +
    + ${esc(priceInfo.main_price || product.price)} + ${esc(product.quantity)} +
    ${esc(priceInfo.price_per_day || '')}
    +
    ${esc(priceInfo.comparison || '')}
    +
    + +
    ${trustBar}
    + + + + + ${pdp.urgency_note ? `
    ⚡ ${esc(pdp.urgency_note)}
    ` : ''} +
    ${esc(pdp.usage_instructions || '')}
    +
    +
    + + +
    +

    Key Benefits

    +
    ${bullets}
    +
    + + +
    ${statsBar}
    + + +
    +

    ${esc(pdp.why_section_title || 'Why This Formula')}

    + ${whyParas} +
    + + +
    +
    +
    ${'★'.repeat(review.stars||5)}
    +

    "${esc(review.text || '')}"

    + — ${esc(review.author || 'Verified Buyer')} +
    +
    + + +
    +

    Frequently Asked Questions

    +
    ${faq}
    +
    + + +
    + 📦 Additional Generated Assets +
    + + + ${meta.title ? ` +
    +

    🔍 Meta SEO

    +
    +
    ${esc(meta.title)}
    +
    justvitamins.co.uk › ${esc(meta.primary_keyword || '')}
    +
    ${esc(meta.description)}
    +
    +
    ` : ''} + + + ${hooks ? ` +
    +

    📢 Ad Hooks

    +
    ${hooks}
    +
    ` : ''} + + + ${emails ? ` +
    +

    📧 Email Sequences

    +
    ${emails}
    +
    ` : ''} + +
    + `; } -function assetCard(type, label, num, content, delay) { - return `
    -
    ${label}#${num}
    -
    ${content}
    `; -} -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 += `
    - ${s.label} -
    ${s.label} — ${s.desc} · ${data.model||''}
    `; - } +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 ''; + + const mainImg = items[0]; + const thumbs = items.map((item, i) => + `
    + ${esc(item.label)} + ${esc(item.label)} +
    ` + ).join(''); + + return ` + +
    ${thumbs}
    + `; } +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', ' 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 => - `
  • ${esc(t.tactic)} — ${esc(t.explanation)}
  • ` - ).join(''); - + `
  • ${esc(t.tactic)} — ${esc(t.explanation)}
  • `).join(''); $('#demoB-left').innerHTML = ` ❌ ${esc(data.competitor_name || 'Competitor')}
    What they're really selling
    @@ -255,52 +347,37 @@ function renderXray(data) {
    Top 5 Persuasion Tactics
      ${tactics}
    Weakest Claim / Gap
    -
    ⚠️ ${esc(data.weakest_claim)}
    - `; +
    ⚠️ ${esc(data.weakest_claim)}
    `; - // Right — JV improved const hero = data.jv_hero_section || {}; const diffs = (data.differentiators||[]).map(d => - `
  • 🎯${esc(d.point)} — ${esc(d.proof_idea)}
  • ` - ).join(''); + `
  • 🎯${esc(d.point)} — ${esc(d.proof_idea)}
  • `).join(''); const donts = (data.do_not_say||[]).map(d => `
  • ${esc(d)}
  • `).join(''); - $('#demoB-right').innerHTML = ` ✓ Just Vitamins — Upgraded -
    -

    ${esc(hero.headline)}

    -

    ${esc(hero.body)}

    -

    ${esc(hero.value_prop)}

    -
    -
    3 Differentiators + Proof Ideas
    -
    -
    ⚠️ Do Not Say — Compliance
    - `; +

    ${esc(hero.headline)}

    ${esc(hero.body)}

    +

    ${esc(hero.value_prop)}

    +
    3 Differentiators
    +
    ⚠️ Do Not Say
    `; } + // ═══════════════════════════════════════════════════════════════ // 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', ' 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', ` 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 => `
  • ${esc(b)}
  • `).join(''); $('#demoC-left').innerHTML = ` ✕ Current PDP + ${product.images?.[0] ? `Current` : ''}

    ${esc(product.title)}

    -

    ${esc(product.subtitle)}

    -

    ${esc(product.price)} ${esc(product.quantity)}

    - -

    ${esc((product.description||'').substring(0,400))}...

    - `; +

    ${esc(product.subtitle)}

    +

    ${esc(product.price)} ${esc(product.quantity)}

    + `; - // Right — rewritten const rBullets = (result.bullets||[]).map(b => - `
    ${esc(b.text)}
    ↑ ${esc(b.annotation)}` - ).join(''); - + `
    ${esc(b.text)}
    ↑ ${esc(b.annotation)}`).join(''); $('#demoC-right').innerHTML = ` - ✓ AI-Rewritten — ${esc(result.style||'balanced').toUpperCase()} + ✓ ${esc(result.style||'balanced').toUpperCase()}

    ${esc(result.title)}

    - ↑ SEO-optimised title -

    ${esc(result.subtitle)}

    - + ↑ SEO title +

    ${esc(result.subtitle)}

    ${esc(result.hero_copy)}
    ↑ ${esc(result.hero_annotation)} - -
    ${rBullets}
    - -
    ⭐ ${esc(result.social_proof)}
    +
    ${rBullets}
    +
    ⭐ ${esc(result.social_proof)}
    ↑ ${esc(result.social_proof_annotation)} - -
    ${esc(result.price_reframe)}
    +
    ${esc(result.price_reframe)}
    ↑ ${esc(result.price_annotation)} - -

    ${esc(result.usage_instruction)}

    +

    ${esc(result.usage_instruction)}

    ↑ ${esc(result.usage_annotation)} - -
    - +
    +
    - `; + ${result.cta_annotation ? `↑ ${esc(result.cta_annotation)}` : ''}`; } -// Smooth scroll nav +// Smooth scroll document.querySelectorAll('a[href^="#"]').forEach(a => { a.addEventListener('click', e => { const t = document.querySelector(a.getAttribute('href')); diff --git a/templates/index.html b/templates/index.html index 31ab308..776891c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -47,8 +47,8 @@
    ⚡ DEMO A — LIVE -

    One Product → 12 Assets in Seconds

    -

    Paste a real Just Vitamins product URL. Gemini scrapes it, then generates a full marketing pack.

    +

    One Product URL → Full Conversion PDP

    +

    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.

    @@ -56,23 +56,17 @@
    - +
    1. ScrapeWaiting
    -
    2. AI GenerateWaiting
    -
    3. Image GenWaiting
    +
    2. PDP CopyWaiting
    +
    3. Product PhotosWaiting