From b2cfdff959abe208719e18cbe51184aec0a52ec1 Mon Sep 17 00:00:00 2001 From: Omair Saleh Date: Thu, 5 Mar 2026 01:58:17 +0800 Subject: [PATCH] Surgical photography: continuous brand experience from landing page to dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ONE image, surgically placed. 'digital-03-notification-smile.jpg' — young man at a London bus stop smiling at his phone. The moment a WhatsApp reminder lands and he thinks 'oh right, I need to do that.' This IS the product working. Not decoration — context. Uses the same image-panel + dark-panel split from the landing page: ┌──────────────┬──────────────────────────────┐ │ [photo] │ ✨ AI optimisation │ │ Man smiling │ Let AI improve your messages │ │ at phone │ [Start optimising] │ └──────────────┴──────────────────────────────┘ The image only appears in the onboarding state (never optimised). Once AI is running, the hero compacts to a single dark bar. The image served its purpose — motivation to start. Brand alignment: - Left-border accent (generosity-gold) on header - 11px uppercase tracking-[0.15em] labels - gap-px grid for timing controls - Sharp edges everywhere (phone mockup is the only exception) - 60-30-10 color rule maintained - Dark inversion for AI hero sections - Typography-driven hierarchy No new images generated. Used existing brand photography. --- .../src/app/dashboard/automations/page.tsx | 466 +++++++++--------- 1 file changed, 226 insertions(+), 240 deletions(-) diff --git a/pledge-now-pay-later/src/app/dashboard/automations/page.tsx b/pledge-now-pay-later/src/app/dashboard/automations/page.tsx index 8a1db38..1fbbc9b 100644 --- a/pledge-now-pay-later/src/app/dashboard/automations/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/automations/page.tsx @@ -1,6 +1,7 @@ "use client" import { useState, useEffect, useCallback, useRef } from "react" +import Image from "next/image" import { Loader2, Check, Send, Sparkles, Trophy, CheckCheck, ChevronDown, Clock, MessageCircle @@ -11,23 +12,16 @@ import { resolvePreview, STEP_META } from "@/lib/templates" /** * /dashboard/automations * - * AI DOES THE WORK. + * AI DOES THE WORK. PHOTOGRAPHY SETS THE CONTEXT. * - * The page has three states: + * The AI hero uses the same image-panel + dark-panel split from + * the landing page. The image is `digital-03-notification-smile` — + * a young man at a bus stop smiling at his phone. It IS the product + * working. The moment a WhatsApp reminder lands and someone thinks + * "oh right, I need to do that." * - * 1. NOT STARTED — big hero: "Let AI improve your messages" - * One button. AI generates challengers for all 4 steps. - * - * 2. TESTING — "AI is testing 4 experiments" - * Each message shows your version vs AI's version with live stats. - * Progress bar toward verdict. - * - * 3. WINNERS — "AI improved your messages by 47%" - * Messages marked with 🏆 badges showing the lift. - * "Run another round" to keep improving. - * - * Aaisha never writes a message. She never picks a winner. - * She just sees: "AI is making your messages better." + * Once AI is running, the hero compacts down — the image served its + * purpose (motivation to start). Now the data takes over. */ interface Template { @@ -81,18 +75,14 @@ export default function AutomationsPage() { templates.find(t => t.step === step && t.channel === "whatsapp" && t.variant === variant) || templates.find(t => t.step === step && t.variant === variant) - // ── Derived state ────────────────────── const testsRunning = STEP_META.filter((_, i) => !!tpl(i, "B")).length const stepsWithoutTest = STEP_META.filter((_, i) => !tpl(i, "B")).length const neverOptimised = testsRunning === 0 && templates.every(t => t.variant === "A") - // ── Actions ──────────────────────────── - const optimiseAll = async () => { setAiWorking(true) - // Generate challengers for all steps that don't have one for (let step = 0; step < 4; step++) { - if (tpl(step, "B")) continue // already has a test + if (tpl(step, "B")) continue try { await fetch("/api/automations/ai", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -153,46 +143,71 @@ export default function AutomationsPage() { const delays = [0, config?.step1Delay || 2, config?.step2Delay || 7, config?.step3Delay || 14] return ( -
+
- {/* Header */} + {/* ── Header ── */}
-

What your donors receive

-

4 messages over {delays[3]} days. Click any to edit.

+
+

Automations

+
+

+ What your donors receive +

+

+ 4 messages over {delays[3]} days. Click any to edit. +

- {/* WhatsApp status */} + {/* ── WhatsApp status ── */} {!waConnected && ( -
-

WhatsApp not connected. Messages start sending once you connect WhatsApp.

+
+

WhatsApp not connected. Messages start once you connect WhatsApp.

)} - {/* ── AI HERO — the main CTA ── */} + {/* ━━ AI HERO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Same image-panel + dark-panel split from the landing page. + The photo is the PRODUCT WORKING — a real person receiving + a WhatsApp reminder and acting on it. + Once AI is running, the hero compacts. The image did its job. + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */} {neverOptimised ? ( - /* State 1: Never optimised */ -
-
- -
-

Let AI improve your messages

-

- AI writes a different version of each message and tests both with real donors. - After enough responses, the better version wins automatically. - Your messages get better over time — without you doing anything. -

-
+
+ {/* Photo — the moment a reminder lands */} +
+ Young man at a London bus stop smiling at his phone — the moment a gentle WhatsApp reminder lands +
+ {/* Dark panel — CTA */} +
+
+ +

AI optimisation

+
+

+ Let AI improve your messages +

+

+ AI writes a different version of each message and tests both with real donors. + The better version wins automatically. Your messages get better over time — without you doing anything. +

+ +

Uses GPT-4.1 nano · Costs less than 1p per message

-
) : testsRunning > 0 ? ( - /* State 2: Tests running */ + /* ── Compact hero: tests running ── */
@@ -200,240 +215,211 @@ export default function AutomationsPage() {

AI is testing {testsRunning} experiment{testsRunning > 1 ? "s" : ""}

-

- Each message has two versions. The better one wins automatically. -

+

Each message has two versions. The better one wins automatically.

{stepsWithoutTest > 0 && ( )}
) : ( - /* State 3: All tests resolved (or manually cleared) */ + /* ── Compact hero: optimised ── */
- +

Messages optimised

-

- {stats && stats.total > 0 - ? `${stats.total} sent this week · ${stats.deliveryRate}% delivered` - : "Winning versions are live. Run another round to keep improving." - } +

+ {stats && stats.total > 0 ? `${stats.total} sent this week · ${stats.deliveryRate}% delivered` : "Winning versions are live."}

)} - {/* ── THE CONVERSATION ── */} -
+ {/* ━━ THE CONVERSATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + The WhatsApp mockup uses rounded corners because it IS a + phone. Everything else follows brand rules (sharp edges). + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */} +
+
- {/* WhatsApp header */} -
- -
- + {/* WhatsApp header */} +
+ +
+ +
+
+

Your charity

+

Automated messages

+
-
-

Your charity

-

Automated messages

-
-
- {/* Chat */} -
- {STEP_META.map((meta, step) => { - const a = tpl(step, "A") - const b = tpl(step, "B") - const isEditing = editing === step - const justSaved = saved === step - const delay = delays[step] - const previewA = a ? resolvePreview(a.body) : "" - const previewB = b ? resolvePreview(b.body) : "" + {/* Chat */} +
+ {STEP_META.map((meta, step) => { + const a = tpl(step, "A") + const b = tpl(step, "B") + const isEditing = editing === step + const justSaved = saved === step + const delay = delays[step] + const previewA = a ? resolvePreview(a.body) : "" + const previewB = b ? resolvePreview(b.body) : "" - // A/B stats - const rateA = a && a.sentCount > 0 ? Math.round((a.convertedCount / a.sentCount) * 100) : 0 - const rateB = b && b.sentCount > 0 ? Math.round((b.convertedCount / b.sentCount) * 100) : 0 - const totalSent = (a?.sentCount || 0) + (b?.sentCount || 0) - const progress = Math.min(100, Math.round((totalSent / (MIN_SAMPLE * 2)) * 100)) - const hasEnoughData = (a?.sentCount || 0) >= MIN_SAMPLE && (b?.sentCount || 0) >= MIN_SAMPLE - const winner = hasEnoughData ? (rateB > rateA ? "B" : rateA > rateB ? "A" : null) : null + const rateA = a && a.sentCount > 0 ? Math.round((a.convertedCount / a.sentCount) * 100) : 0 + const rateB = b && b.sentCount > 0 ? Math.round((b.convertedCount / b.sentCount) * 100) : 0 + const totalSent = (a?.sentCount || 0) + (b?.sentCount || 0) + const progress = Math.min(100, Math.round((totalSent / (MIN_SAMPLE * 2)) * 100)) + const hasEnoughData = (a?.sentCount || 0) >= MIN_SAMPLE && (b?.sentCount || 0) >= MIN_SAMPLE + const winner = hasEnoughData ? (rateB > rateA ? "B" : rateA > rateB ? "A" : null) : null - return ( -
- {/* Timestamp */} -
- - {step === 0 ? "Instantly" : `Day ${delay} · if not paid`} - -
+ return ( +
+ {/* Timestamp */} +
+ + {step === 0 ? "Instantly" : `Day ${delay} · if not paid`} + +
- {isEditing ? ( - /* ── Editing ── */ -
-
-