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 ee7d8ba..8a1db38 100644 --- a/pledge-now-pay-later/src/app/dashboard/automations/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/automations/page.tsx @@ -11,18 +11,23 @@ import { resolvePreview, STEP_META } from "@/lib/templates" /** * /dashboard/automations * - * THE PAGE IS THE CONVERSATION. + * AI DOES THE WORK. * - * Aaisha's question: "What do my donors get?" - * Answer: a WhatsApp chat showing all 4 messages, in order, - * with timestamps between them. That's the entire page. + * The page has three states: * - * Click a message → it becomes editable (inline). - * Click ✨ → AI generates a smarter version. - * That's it. + * 1. NOT STARTED — big hero: "Let AI improve your messages" + * One button. AI generates challengers for all 4 steps. * - * No tabs. No matrix. No toolbar. No panels. - * Just a phone with the messages your donors receive. + * 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." */ interface Template { @@ -40,6 +45,8 @@ interface Config { interface ChannelStatus { whatsapp: boolean; email: { provider: string; fromAddress: string } | null; sms: { provider: string; fromNumber: string } | null } interface Stats { whatsapp: { sent: number; failed: number }; email: { sent: number; failed: number }; sms: { sent: number; failed: number }; total: number; deliveryRate: number } +const MIN_SAMPLE = 20 + export default function AutomationsPage() { const [loading, setLoading] = useState(true) const [templates, setTemplates] = useState([]) @@ -47,11 +54,11 @@ export default function AutomationsPage() { const [channels, setChannels] = useState(null) const [stats, setStats] = useState(null) - const [editing, setEditing] = useState(null) // step being edited + const [editing, setEditing] = useState(null) const [editBody, setEditBody] = useState("") const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(null) - const [aiLoading, setAiLoading] = useState(null) // step being AI'd + const [aiWorking, setAiWorking] = useState(false) const [showTiming, setShowTiming] = useState(false) const editorRef = useRef(null) @@ -70,19 +77,51 @@ export default function AutomationsPage() { useEffect(() => { load() }, [load]) - // Get template for a step (WhatsApp variant A preferred) const tpl = (step: number, variant = "A") => 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 + try { + await fetch("/api/automations/ai", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "generate_variant", step, channel: "whatsapp" }), + }) + } catch { /* */ } + } + await load() + setAiWorking(false) + } + + const pickWinnersAndContinue = async () => { + setAiWorking(true) + try { + await fetch("/api/automations/ai", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "check_winners" }), + }) + } catch { /* */ } + await load() + setAiWorking(false) + } + const startEdit = (step: number) => { const t = tpl(step) if (t) { setEditBody(t.body); setEditing(step) } setTimeout(() => editorRef.current?.focus(), 50) } - const cancelEdit = () => { setEditing(null); setEditBody("") } - const saveEdit = async (step: number) => { setSaving(true) const t = tpl(step) @@ -98,41 +137,6 @@ export default function AutomationsPage() { setSaving(false) } - const aiGenerate = async (step: number) => { - setAiLoading(step) - try { - const res = await fetch("/api/automations/ai", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "generate_variant", step, channel: "whatsapp" }), - }) - const data = await res.json() - if (data.ok) await load() - } catch { /* */ } - setAiLoading(null) - } - - const pickWinners = async () => { - setAiLoading(-1) - try { - const res = await fetch("/api/automations/ai", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "check_winners" }), - }) - if (res.ok) await load() - } catch { /* */ } - setAiLoading(null) - } - - const removeVariant = async (step: number) => { - try { - await fetch("/api/automations", { - method: "DELETE", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ step, channel: "whatsapp", variant: "B" }), - }) - await load() - } catch { /* */ } - } - const saveTiming = async (key: string, value: number) => { try { await fetch("/api/automations", { @@ -146,39 +150,85 @@ export default function AutomationsPage() { if (loading) return
const waConnected = !!channels?.whatsapp - const anyAB = templates.some(t => t.variant === "B") const delays = [0, config?.step1Delay || 2, config?.step2Delay || 7, config?.step3Delay || 14] return (
- {/* Header — one line */} + {/* Header */}

What your donors receive

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

- {/* Status line */} - {!waConnected ? ( + {/* WhatsApp status */} + {!waConnected && (
-

WhatsApp not connected. These messages will start sending once you connect WhatsApp.

+

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

- ) : stats && stats.total > 0 ? ( -

- Working · {stats.total} sent this week · {stats.deliveryRate}% delivered -

- ) : ( -

- Connected · Messages will send as donors pledge -

)} - {/* Pick winners button — only when A/B tests exist */} - {anyAB && ( - + {/* ── AI HERO — the main CTA ── */} + {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. +

+
+
+ +
+ ) : testsRunning > 0 ? ( + /* State 2: Tests running */ +
+
+ + +
+
+

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

+

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

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

Messages optimised

+

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

+
+ +
)} {/* ── THE CONVERSATION ── */} @@ -196,7 +246,7 @@ export default function AutomationsPage() {
- {/* Chat area */} + {/* Chat */}
@@ -205,67 +255,123 @@ export default function AutomationsPage() { const b = tpl(step, "B") const isEditing = editing === step const justSaved = saved === step - const isAiLoading = aiLoading === 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 + return (
- {/* Timestamp divider */} + {/* Timestamp */}
{step === 0 ? "Instantly" : `Day ${delay} · if not paid`}
- {/* A/B split — two bubbles stacked */} - {b && !isEditing ? ( -
- startEdit(step)} - /> - startEdit(step)} isAI - /> - -
- ) : isEditing ? ( - /* ── Editing mode ── */ + {isEditing ? ( + /* ── Editing ── */
-