Surgical photography: continuous brand experience from landing page to dashboard
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.
This commit is contained in:
@@ -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 (
|
||||
<div className="max-w-lg mx-auto space-y-5">
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Header */}
|
||||
{/* ── Header ── */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-[#111827] tracking-tight">What your donors receive</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">4 messages over {delays[3]} days. Click any to edit.</p>
|
||||
<div className="border-l-2 border-[#F59E0B] pl-3 mb-3">
|
||||
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Automations</p>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-black text-[#111827] tracking-tight">
|
||||
What your donors receive
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
4 messages over {delays[3]} days. Click any to edit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* WhatsApp status */}
|
||||
{/* ── WhatsApp status ── */}
|
||||
{!waConnected && (
|
||||
<div className="border-l-2 border-[#F59E0B] bg-[#FEF3C7] px-4 py-3">
|
||||
<p className="text-xs text-[#111827]"><strong>WhatsApp not connected.</strong> Messages start sending once you <Link href="/dashboard/settings" className="text-[#1E40AF] font-bold underline">connect WhatsApp</Link>.</p>
|
||||
<div className="border-l-2 border-[#F59E0B] pl-4 py-2">
|
||||
<p className="text-sm text-gray-600"><strong className="text-[#111827]">WhatsApp not connected.</strong> Messages start once you <Link href="/dashboard/settings" className="text-[#1E40AF] font-bold hover:underline">connect WhatsApp</Link>.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 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 */
|
||||
<div className="bg-[#111827] p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className="h-5 w-5 text-[#60A5FA] mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white">Let AI improve your messages</p>
|
||||
<p className="text-xs text-gray-400 mt-1 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-5 gap-0">
|
||||
{/* Photo — the moment a reminder lands */}
|
||||
<div className="md:col-span-2 relative min-h-[200px] md:min-h-[280px] overflow-hidden">
|
||||
<Image
|
||||
src="/images/brand/digital-03-notification-smile.jpg"
|
||||
alt="Young man at a London bus stop smiling at his phone — the moment a gentle WhatsApp reminder lands"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 40vw"
|
||||
/>
|
||||
</div>
|
||||
{/* Dark panel — CTA */}
|
||||
<div className="md:col-span-3 bg-[#111827] p-8 md:p-10 flex flex-col justify-center">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Sparkles className="h-4 w-4 text-[#60A5FA]" />
|
||||
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">AI optimisation</p>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-black text-white tracking-tight">
|
||||
Let AI improve your messages
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 leading-relaxed mt-3 max-w-md">
|
||||
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.
|
||||
</p>
|
||||
<button onClick={optimiseAll} disabled={aiWorking}
|
||||
className="mt-6 inline-flex items-center justify-center bg-white px-6 py-3 text-sm font-bold text-[#111827] hover:bg-gray-100 transition-colors self-start disabled:opacity-60">
|
||||
{aiWorking
|
||||
? <><Loader2 className="h-4 w-4 animate-spin mr-2" /> AI is writing new versions…</>
|
||||
: <><Sparkles className="h-4 w-4 mr-2" /> Start optimising</>
|
||||
}
|
||||
</button>
|
||||
<p className="text-[11px] text-gray-500 mt-3">Uses GPT-4.1 nano · Costs less than 1p per message</p>
|
||||
</div>
|
||||
<button onClick={optimiseAll} disabled={aiWorking}
|
||||
className="mt-4 w-full bg-white text-[#111827] py-3 text-sm font-bold flex items-center justify-center gap-2 hover:bg-gray-100 transition-colors disabled:opacity-60">
|
||||
{aiWorking
|
||||
? <><Loader2 className="h-4 w-4 animate-spin" /> AI is writing new versions…</>
|
||||
: <><Sparkles className="h-4 w-4" /> Start optimising</>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
) : testsRunning > 0 ? (
|
||||
/* State 2: Tests running */
|
||||
/* ── Compact hero: tests running ── */
|
||||
<div className="bg-[#111827] p-5 flex items-center gap-4">
|
||||
<div className="relative shrink-0">
|
||||
<Sparkles className="h-5 w-5 text-[#60A5FA]" />
|
||||
@@ -200,240 +215,211 @@ export default function AutomationsPage() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-bold text-white">AI is testing {testsRunning} experiment{testsRunning > 1 ? "s" : ""}</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
Each message has two versions. The better one wins automatically.
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">Each message has two versions. The better one wins automatically.</p>
|
||||
</div>
|
||||
{stepsWithoutTest > 0 && (
|
||||
<button onClick={optimiseAll} disabled={aiWorking}
|
||||
className="shrink-0 bg-white/10 text-white px-3 py-1.5 text-[10px] font-bold hover:bg-white/20 transition-colors disabled:opacity-50">
|
||||
className="shrink-0 border border-gray-600 text-gray-300 px-3 py-1.5 text-[11px] font-bold hover:text-white hover:border-white transition-colors disabled:opacity-50">
|
||||
{aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : `+ ${stepsWithoutTest} more`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* State 3: All tests resolved (or manually cleared) */
|
||||
/* ── Compact hero: optimised ── */
|
||||
<div className="bg-[#111827] p-5 flex items-center gap-4">
|
||||
<Trophy className="h-5 w-5 text-[#4ADE80] shrink-0" />
|
||||
<Trophy className="h-5 w-5 text-[#16A34A] shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-bold text-white">Messages optimised</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
{stats && stats.total > 0
|
||||
? `${stats.total} sent this week · ${stats.deliveryRate}% delivered`
|
||||
: "Winning versions are live. Run another round to keep improving."
|
||||
}
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">
|
||||
{stats && stats.total > 0 ? `${stats.total} sent this week · ${stats.deliveryRate}% delivered` : "Winning versions are live."}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={optimiseAll} disabled={aiWorking}
|
||||
className="shrink-0 bg-white/10 text-white px-3 py-1.5 text-[10px] font-bold hover:bg-white/20 transition-colors disabled:opacity-50 flex items-center gap-1">
|
||||
className="shrink-0 border border-gray-600 text-gray-300 px-3 py-1.5 text-[11px] font-bold hover:text-white hover:border-white transition-colors disabled:opacity-50 flex items-center gap-1.5">
|
||||
{aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : <><Sparkles className="h-3 w-3" /> New round</>}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── THE CONVERSATION ── */}
|
||||
<div className="border border-gray-300 overflow-hidden shadow-lg" style={{ borderRadius: "20px" }}>
|
||||
{/* ━━ THE CONVERSATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
The WhatsApp mockup uses rounded corners because it IS a
|
||||
phone. Everything else follows brand rules (sharp edges).
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}
|
||||
<div className="max-w-lg mx-auto">
|
||||
<div className="border border-gray-300 overflow-hidden shadow-lg" style={{ borderRadius: "20px" }}>
|
||||
|
||||
{/* WhatsApp header */}
|
||||
<div className="bg-[#075E54] px-4 py-3 flex items-center gap-3">
|
||||
<span className="text-white/50 text-sm">←</span>
|
||||
<div className="w-9 h-9 bg-[#128C7E] flex items-center justify-center" style={{ borderRadius: "50%" }}>
|
||||
<MessageCircle className="h-4 w-4 text-white" />
|
||||
{/* WhatsApp header */}
|
||||
<div className="bg-[#075E54] px-4 py-3 flex items-center gap-3">
|
||||
<span className="text-white/50 text-sm">←</span>
|
||||
<div className="w-9 h-9 bg-[#128C7E] flex items-center justify-center" style={{ borderRadius: "50%" }}>
|
||||
<MessageCircle className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white text-sm font-medium">Your charity</p>
|
||||
<p className="text-[10px] text-white/50">Automated messages</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white text-sm font-medium">Your charity</p>
|
||||
<p className="text-[10px] text-white/50">Automated messages</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat */}
|
||||
<div className="bg-[#ECE5DD] px-4 py-4 space-y-4" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||
}}>
|
||||
{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 */}
|
||||
<div className="bg-[#ECE5DD] px-4 py-4 space-y-4" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||
}}>
|
||||
{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 (
|
||||
<div key={step}>
|
||||
{/* Timestamp */}
|
||||
<div className="flex justify-center mb-3">
|
||||
<span className="bg-white/80 text-[10px] text-[#667781] px-3 py-1 font-medium shadow-sm" style={{ borderRadius: "6px" }}>
|
||||
{step === 0 ? "Instantly" : `Day ${delay} · if not paid`}
|
||||
</span>
|
||||
</div>
|
||||
return (
|
||||
<div key={step}>
|
||||
{/* Timestamp */}
|
||||
<div className="flex justify-center mb-3">
|
||||
<span className="bg-white/80 text-[10px] text-[#667781] px-3 py-1 font-medium shadow-sm" style={{ borderRadius: "6px" }}>
|
||||
{step === 0 ? "Instantly" : `Day ${delay} · if not paid`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
/* ── Editing ── */
|
||||
<div className="flex justify-end">
|
||||
<div className="bg-[#DCF8C6] max-w-[90%] w-full shadow-sm" style={{ borderRadius: "8px 0 8px 8px" }}>
|
||||
<textarea ref={editorRef} value={editBody} onChange={e => setEditBody(e.target.value)}
|
||||
className="w-full bg-transparent px-3 py-2 text-[12px] leading-[1.5] text-[#303030] resize-y outline-none min-h-[120px] font-mono" />
|
||||
<div className="px-3 pb-2 flex items-center gap-2">
|
||||
<button onClick={() => saveEdit(step)} disabled={saving}
|
||||
className="bg-[#075E54] text-white px-3 py-1.5 text-[10px] font-bold flex items-center gap-1 disabled:opacity-50" style={{ borderRadius: "4px" }}>
|
||||
{saving ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <Check className="h-2.5 w-2.5" />} Save
|
||||
</button>
|
||||
<button onClick={() => setEditing(null)} className="text-[10px] text-[#667781]">Cancel</button>
|
||||
<span className="text-[9px] text-[#667781]/50 ml-auto">{"{{name}} {{amount}} {{reference}}"}</span>
|
||||
{isEditing ? (
|
||||
<div className="flex justify-end">
|
||||
<div className="bg-[#DCF8C6] max-w-[90%] w-full shadow-sm" style={{ borderRadius: "8px 0 8px 8px" }}>
|
||||
<textarea ref={editorRef} value={editBody} onChange={e => setEditBody(e.target.value)}
|
||||
className="w-full bg-transparent px-3 py-2 text-[12px] leading-[1.5] text-[#303030] resize-y outline-none min-h-[120px] font-mono" />
|
||||
<div className="px-3 pb-2 flex items-center gap-2">
|
||||
<button onClick={() => saveEdit(step)} disabled={saving}
|
||||
className="bg-[#075E54] text-white px-3 py-1.5 text-[10px] font-bold flex items-center gap-1 disabled:opacity-50" style={{ borderRadius: "4px" }}>
|
||||
{saving ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <Check className="h-2.5 w-2.5" />} Save
|
||||
</button>
|
||||
<button onClick={() => setEditing(null)} className="text-[10px] text-[#667781]">Cancel</button>
|
||||
<span className="text-[9px] text-[#667781]/50 ml-auto">{"{{name}} {{amount}} {{reference}}"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : b ? (
|
||||
/* ── A/B test in progress ── */
|
||||
<div className="flex justify-end">
|
||||
<div className="max-w-[90%] w-full" style={{ borderRadius: "8px" }}>
|
||||
{/* Test header */}
|
||||
<div className="bg-[#075E54] text-white px-3 py-1.5 flex items-center gap-1.5" style={{ borderRadius: "8px 8px 0 0" }}>
|
||||
<Sparkles className="h-3 w-3 text-[#60A5FA]" />
|
||||
<span className="text-[10px] font-bold flex-1">AI is testing this message</span>
|
||||
{hasEnoughData && winner && (
|
||||
<span className="text-[9px] bg-[#4ADE80]/20 text-[#4ADE80] px-1.5 py-0.5 font-bold flex items-center gap-0.5">
|
||||
<Trophy className="h-2.5 w-2.5" /> {winner === "B" ? "AI" : "Yours"} winning
|
||||
</span>
|
||||
)}
|
||||
{!hasEnoughData && <span className="text-[9px] text-white/40">{progress}%</span>}
|
||||
</div>
|
||||
|
||||
{/* Two versions side by side */}
|
||||
<div className="grid grid-cols-2 gap-px bg-[#075E54]/20">
|
||||
{/* Your version */}
|
||||
<button onClick={() => startEdit(step)} className="bg-[#DCF8C6] p-2.5 text-left hover:brightness-[0.97] transition-all">
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<span className="text-[8px] font-bold text-[#075E54] bg-[#075E54]/10 px-1.5 py-0.5">Yours</span>
|
||||
{a && a.sentCount > 0 && (
|
||||
<span className={`text-[9px] font-bold ml-auto ${winner === "A" ? "text-[#075E54]" : "text-[#667781]"}`}>
|
||||
{rateA}% {winner === "A" && "🏆"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] leading-[1.4] text-[#303030] line-clamp-4">
|
||||
<WhatsAppFormatted text={previewA} />
|
||||
</div>
|
||||
{a && a.sentCount > 0 && (
|
||||
<p className="text-[8px] text-[#667781] mt-1">{a.convertedCount} paid / {a.sentCount} sent</p>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* AI version */}
|
||||
<div className="bg-[#DCF8C6] p-2.5">
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<span className="text-[8px] font-bold text-[#1E40AF] bg-[#1E40AF]/10 px-1.5 py-0.5 flex items-center gap-0.5">
|
||||
<Sparkles className="h-2 w-2" /> AI
|
||||
) : b ? (
|
||||
<div className="flex justify-end">
|
||||
<div className="max-w-[90%] w-full" style={{ borderRadius: "8px" }}>
|
||||
<div className="bg-[#075E54] text-white px-3 py-1.5 flex items-center gap-1.5" style={{ borderRadius: "8px 8px 0 0" }}>
|
||||
<Sparkles className="h-3 w-3 text-[#60A5FA]" />
|
||||
<span className="text-[10px] font-bold flex-1">AI is testing</span>
|
||||
{hasEnoughData && winner && (
|
||||
<span className="text-[9px] bg-[#16A34A]/20 text-[#4ADE80] px-1.5 py-0.5 font-bold flex items-center gap-0.5">
|
||||
<Trophy className="h-2.5 w-2.5" /> {winner === "B" ? "AI" : "Yours"} winning
|
||||
</span>
|
||||
{b.sentCount > 0 && (
|
||||
<span className={`text-[9px] font-bold ml-auto ${winner === "B" ? "text-[#075E54]" : "text-[#667781]"}`}>
|
||||
{rateB}% {winner === "B" && "🏆"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] leading-[1.4] text-[#303030] line-clamp-4">
|
||||
<WhatsAppFormatted text={previewB} />
|
||||
</div>
|
||||
{b.sentCount > 0 && (
|
||||
<p className="text-[8px] text-[#667781] mt-1">{b.convertedCount} paid / {b.sentCount} sent</p>
|
||||
)}
|
||||
{!hasEnoughData && <span className="text-[9px] text-white/40">{progress}%</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="bg-white/60 px-3 py-1.5" style={{ borderRadius: "0 0 8px 8px" }}>
|
||||
<div className="h-1 bg-gray-200 overflow-hidden" style={{ borderRadius: "2px" }}>
|
||||
<div className={`h-full transition-all ${hasEnoughData ? "bg-[#4ADE80]" : "bg-[#60A5FA]"}`}
|
||||
style={{ width: `${progress}%` }} />
|
||||
<div className="grid grid-cols-2 gap-px bg-[#075E54]/20">
|
||||
<button onClick={() => startEdit(step)} className="bg-[#DCF8C6] p-2.5 text-left hover:brightness-[0.97] transition-all">
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<span className="text-[8px] font-bold text-[#075E54] bg-[#075E54]/10 px-1.5 py-0.5">Yours</span>
|
||||
{a && a.sentCount > 0 && <span className={`text-[9px] font-bold ml-auto ${winner === "A" ? "text-[#075E54]" : "text-[#667781]"}`}>{rateA}%{winner === "A" && " 🏆"}</span>}
|
||||
</div>
|
||||
<div className="text-[10px] leading-[1.4] text-[#303030] line-clamp-4"><WhatsAppFormatted text={previewA} /></div>
|
||||
{a && a.sentCount > 0 && <p className="text-[8px] text-[#667781] mt-1">{a.convertedCount}/{a.sentCount} paid</p>}
|
||||
</button>
|
||||
<div className="bg-[#DCF8C6] p-2.5">
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<span className="text-[8px] font-bold text-[#1E40AF] bg-[#1E40AF]/10 px-1.5 py-0.5 flex items-center gap-0.5"><Sparkles className="h-2 w-2" /> AI</span>
|
||||
{b.sentCount > 0 && <span className={`text-[9px] font-bold ml-auto ${winner === "B" ? "text-[#075E54]" : "text-[#667781]"}`}>{rateB}%{winner === "B" && " 🏆"}</span>}
|
||||
</div>
|
||||
<div className="text-[10px] leading-[1.4] text-[#303030] line-clamp-4"><WhatsAppFormatted text={previewB} /></div>
|
||||
{b.sentCount > 0 && <p className="text-[8px] text-[#667781] mt-1">{b.convertedCount}/{b.sentCount} paid</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/60 px-3 py-1.5" style={{ borderRadius: "0 0 8px 8px" }}>
|
||||
<div className="h-1 bg-gray-200 overflow-hidden" style={{ borderRadius: "2px" }}>
|
||||
<div className={`h-full transition-all ${hasEnoughData ? "bg-[#16A34A]" : "bg-[#60A5FA]"}`} style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
<p className="text-[8px] text-[#667781] mt-1">
|
||||
{hasEnoughData
|
||||
? winner ? `${winner === "B" ? "AI" : "Your"} version converts ${Math.abs(rateB - rateA)}% better` : "Too close to call"
|
||||
: `${totalSent} of ${MIN_SAMPLE * 2} sends to verdict`}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[8px] text-[#667781] mt-1">
|
||||
{hasEnoughData
|
||||
? winner
|
||||
? `${winner === "B" ? "AI" : "Your"} version converts ${Math.abs(rateB - rateA)}% better`
|
||||
: "Too close to call — collecting more data"
|
||||
: `${totalSent} of ${MIN_SAMPLE * 2} sends needed for verdict`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Normal bubble ── */
|
||||
<div className="flex justify-end">
|
||||
<button onClick={() => startEdit(step)}
|
||||
className="bg-[#DCF8C6] max-w-[85%] px-3 py-2 text-left text-[12px] leading-[1.45] text-[#303030] relative shadow-sm cursor-pointer hover:brightness-[0.97] transition-all"
|
||||
style={{ borderRadius: "8px 0 8px 8px" }}>
|
||||
{justSaved && (
|
||||
<div className="absolute -top-2 -right-2 w-5 h-5 bg-[#25D366] flex items-center justify-center" style={{ borderRadius: "50%" }}>
|
||||
<Check className="h-3 w-3 text-white" />
|
||||
) : (
|
||||
<div className="flex justify-end">
|
||||
<button onClick={() => startEdit(step)}
|
||||
className="bg-[#DCF8C6] max-w-[85%] px-3 py-2 text-left text-[12px] leading-[1.45] text-[#303030] relative shadow-sm cursor-pointer hover:brightness-[0.97] transition-all"
|
||||
style={{ borderRadius: "8px 0 8px 8px" }}>
|
||||
{justSaved && (
|
||||
<div className="absolute -top-2 -right-2 w-5 h-5 bg-[#25D366] flex items-center justify-center" style={{ borderRadius: "50%" }}>
|
||||
<Check className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<WhatsAppFormatted text={previewA} />
|
||||
<div className="flex items-center justify-end gap-1 mt-1 -mb-0.5">
|
||||
<span className="text-[9px] text-[#667781]">{step === 0 ? "09:41" : step === 1 ? "10:15" : step === 2 ? "09:30" : "11:00"}</span>
|
||||
<CheckCheck className="h-3 w-3 text-[#53BDEB]" />
|
||||
</div>
|
||||
)}
|
||||
<WhatsAppFormatted text={previewA} />
|
||||
<div className="flex items-center justify-end gap-1 mt-1 -mb-0.5">
|
||||
<span className="text-[9px] text-[#667781]">{step === 0 ? "09:41" : step === 1 ? "10:15" : step === 2 ? "09:30" : "11:00"}</span>
|
||||
<CheckCheck className="h-3 w-3 text-[#53BDEB]" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Input bar */}
|
||||
<div className="bg-[#F0F0F0] px-3 py-2 flex items-center gap-2">
|
||||
<div className="flex-1 bg-white px-4 py-2 text-[11px] text-[#667781]" style={{ borderRadius: "20px" }}>
|
||||
Donors can reply: PAID · HELP · CANCEL
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="w-9 h-9 bg-[#075E54] flex items-center justify-center shrink-0" style={{ borderRadius: "50%" }}>
|
||||
<Send className="h-4 w-4 text-white ml-0.5" />
|
||||
|
||||
{/* Input bar */}
|
||||
<div className="bg-[#F0F0F0] px-3 py-2 flex items-center gap-2">
|
||||
<div className="flex-1 bg-white px-4 py-2 text-[11px] text-[#667781]" style={{ borderRadius: "20px" }}>
|
||||
Donors can reply: PAID · HELP · CANCEL
|
||||
</div>
|
||||
<div className="w-9 h-9 bg-[#075E54] flex items-center justify-center shrink-0" style={{ borderRadius: "50%" }}>
|
||||
<Send className="h-4 w-4 text-white ml-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pick winners — only when tests have enough data */}
|
||||
{/* ── Pick winners ── */}
|
||||
{testsRunning > 0 && (
|
||||
<button onClick={pickWinnersAndContinue} disabled={aiWorking}
|
||||
className="w-full bg-[#111827] text-white py-3 text-xs font-bold flex items-center justify-center gap-2 hover:bg-gray-800 transition-colors disabled:opacity-50">
|
||||
{aiWorking
|
||||
? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Picking winners…</>
|
||||
: <><Trophy className="h-3.5 w-3.5" /> Pick winners & start new round</>
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Timing */}
|
||||
<button onClick={() => setShowTiming(!showTiming)} className="w-full text-left flex items-center gap-2 text-xs text-gray-400 hover:text-gray-600 transition-colors py-1">
|
||||
<Clock className="h-3 w-3" /> Change timing
|
||||
<ChevronDown className={`h-3 w-3 transition-transform ${showTiming ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{showTiming && (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ label: "First reminder", key: "step1Delay", value: config?.step1Delay || 2 },
|
||||
{ label: "Second reminder", key: "step2Delay", value: config?.step2Delay || 7 },
|
||||
{ label: "Final reminder", key: "step3Delay", value: config?.step3Delay || 14 },
|
||||
].map(t => (
|
||||
<div key={t.key}>
|
||||
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-wide mb-1">{t.label}</p>
|
||||
<select value={t.value} onChange={e => saveTiming(t.key, parseInt(e.target.value))}
|
||||
className="w-full border-2 border-gray-200 px-2 py-1.5 text-xs font-bold text-[#111827] bg-white focus:border-[#1E40AF] outline-none">
|
||||
{[1, 2, 3, 5, 7, 10, 14, 21, 28].map(d => <option key={d} value={d}>Day {d}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
<div className="max-w-lg mx-auto">
|
||||
<button onClick={pickWinnersAndContinue} disabled={aiWorking}
|
||||
className="w-full bg-[#111827] text-white py-3 text-sm font-bold flex items-center justify-center gap-2 hover:bg-gray-800 transition-colors disabled:opacity-50">
|
||||
{aiWorking
|
||||
? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Picking winners…</>
|
||||
: <><Trophy className="h-3.5 w-3.5" /> Pick winners & start new round</>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Timing (expandable, in brand language) ── */}
|
||||
<div className="max-w-lg mx-auto">
|
||||
<button onClick={() => setShowTiming(!showTiming)}
|
||||
className="flex items-center gap-2 text-[11px] text-gray-400 hover:text-gray-600 transition-colors py-1 font-semibold tracking-wide uppercase">
|
||||
<Clock className="h-3 w-3" /> Change timing
|
||||
<ChevronDown className={`h-3 w-3 transition-transform ${showTiming ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{showTiming && (
|
||||
<div className="grid grid-cols-3 gap-px bg-gray-200 mt-3">
|
||||
{[
|
||||
{ label: "First reminder", key: "step1Delay", value: config?.step1Delay || 2 },
|
||||
{ label: "Second reminder", key: "step2Delay", value: config?.step2Delay || 7 },
|
||||
{ label: "Final reminder", key: "step3Delay", value: config?.step3Delay || 14 },
|
||||
].map(t => (
|
||||
<div key={t.key} className="bg-white p-4">
|
||||
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-wide mb-2">{t.label}</p>
|
||||
<select value={t.value} onChange={e => saveTiming(t.key, parseInt(e.target.value))}
|
||||
className="w-full border-2 border-gray-200 px-2 py-1.5 text-xs font-bold text-[#111827] bg-white focus:border-[#1E40AF] outline-none">
|
||||
{[1, 2, 3, 5, 7, 10, 14, 21, 28].map(d => <option key={d} value={d}>Day {d}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user