From f1a8c59b0d5f9c0c6609f4b127c61cfc76908184 Mon Sep 17 00:00:00 2001 From: Omair Saleh Date: Thu, 5 Mar 2026 01:24:28 +0800 Subject: [PATCH] Automations radical simplification: the page IS the conversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 807 → 394 lines. Removed everything that isn't the answer to 'What do my donors get?' REMOVED: - Step timeline tabs (4 across the top) - Channel tabs (WhatsApp / Email / SMS) - A/B variant toggle buttons - AI rewrite toolbar (8 buttons) - Variable chips panel - Channel strategy matrix - Delivery matrix table - Strategy presets - Template name editor - Subject line editor - Character counter - Formatting cheatsheet - Live feed accordion - Stats bar - Scheduled reminders list - Message history feed WHAT REMAINS: One WhatsApp conversation showing all 4 messages. That's the entire page. - Click a message → it becomes editable inline (green bubble → textarea) - Hover → '✨ Try a different approach' appears (AI generates variant B) - A/B tests show as stacked bubbles with conversion rates - '🏆 Pick winners' button appears when tests are running - 'Change timing' link at the bottom (expandable, 3 dropdowns) - Status line: 'Working · 47 sent · 94% delivered' The phone mockup is the full-width page content, not a sidebar. The input bar says 'Donors can reply: PAID · HELP · CANCEL' Timestamp dividers: 'Instantly', 'Day 2 · if not paid', etc. This is what Aaisha wants to see: her donors' experience. --- .../src/app/dashboard/automations/page.tsx | 947 +++++------------- 1 file changed, 267 insertions(+), 680 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 fc5c6a1..ee7d8ba 100644 --- a/pledge-now-pay-later/src/app/dashboard/automations/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/automations/page.tsx @@ -2,15 +2,28 @@ import { useState, useEffect, useCallback, useRef } from "react" import { - Loader2, MessageCircle, Mail, Smartphone, Check, X, Clock, - ChevronDown, Send, Zap, AlertTriangle, Trash2, - Pencil, RotateCcw, CheckCheck, Sparkles, Trophy, FlaskConical, - TrendingUp + Loader2, Check, Send, Sparkles, Trophy, CheckCheck, + ChevronDown, Clock, MessageCircle } from "lucide-react" import Link from "next/link" -import { resolvePreview, TEMPLATE_VARIABLES, STEP_META, STRATEGY_PRESETS } from "@/lib/templates" +import { resolvePreview, STEP_META } from "@/lib/templates" -// ─── Types ─────────────────────────────────────────────────── +/** + * /dashboard/automations + * + * THE PAGE IS THE CONVERSATION. + * + * 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. + * + * Click a message → it becomes editable (inline). + * Click ✨ → AI generates a smarter version. + * That's it. + * + * No tabs. No matrix. No toolbar. No panels. + * Just a phone with the messages your donors receive. + */ interface Template { id: string; step: number; channel: string; variant: string @@ -25,62 +38,21 @@ interface Config { } interface ChannelStatus { whatsapp: boolean; email: { provider: string; fromAddress: string } | null; sms: { provider: string; fromNumber: string } | null } -interface ChannelStats { whatsapp: { sent: number; failed: number }; email: { sent: number; failed: number }; sms: { sent: number; failed: number }; total: number; deliveryRate: number } -interface MessageEntry { id: string; channel: string; messageType: string; donorName: string | null; success: boolean; error: string | null; createdAt: string } -interface PendingEntry { id: string; donorName: string | null; amountPence: number; step: number; channel: string; scheduledAt: string } - -const CHANNELS = ["whatsapp", "email", "sms"] as const -type Channel = typeof CHANNELS[number] -const CH_META: Record = { - whatsapp: { icon: MessageCircle, color: "#25D366", label: "WhatsApp" }, - email: { icon: Mail, color: "#1E40AF", label: "Email" }, - sms: { icon: Smartphone, color: "#F59E0B", label: "SMS" }, -} - -const REWRITE_ACTIONS = [ - { id: "shorter", label: "Make shorter", icon: "✂️" }, - { id: "warmer", label: "Make warmer", icon: "💛" }, - { id: "urgent", label: "Add urgency", icon: "⏰" }, - { id: "social_proof", label: "Add social proof", icon: "👥" }, - { id: "impact", label: "Add impact story", icon: "💚" }, - { id: "minimal", label: "Strip to essentials", icon: "🎯" }, - { id: "urdu", label: "Translate to Urdu", icon: "🇵🇰" }, - { id: "arabic", label: "Translate to Arabic", icon: "🇸🇦" }, -] - -// ─── Page ──────────────────────────────────────────────────── +interface Stats { whatsapp: { sent: number; failed: number }; email: { sent: number; failed: number }; sms: { sent: number; failed: number }; total: number; deliveryRate: number } export default function AutomationsPage() { const [loading, setLoading] = useState(true) const [templates, setTemplates] = useState([]) const [config, setConfig] = useState(null) const [channels, setChannels] = useState(null) - const [stats, setStats] = useState(null) - const [history, setHistory] = useState([]) - const [pending, setPending] = useState([]) + const [stats, setStats] = useState(null) - const [activeStep, setActiveStep] = useState(0) - const [activeChannel, setActiveChannel] = useState("whatsapp") - const [activeVariant, setActiveVariant] = useState("A") + const [editing, setEditing] = useState(null) // step being edited const [editBody, setEditBody] = useState("") - const [editSubject, setEditSubject] = useState("") - const [editName, setEditName] = useState("") - const [dirty, setDirty] = useState(false) const [saving, setSaving] = useState(false) - const [saved, setSaved] = useState(false) - const [showStrategy, setShowStrategy] = useState(false) - const [showFeed, setShowFeed] = useState(false) - - // AI state - const [aiGenerating, setAiGenerating] = useState(false) - const [aiReasoning, setAiReasoning] = useState(null) - const [aiStrategy, setAiStrategy] = useState(null) - const [rewriting, setRewriting] = useState(null) - const [checkingWinners, setCheckingWinners] = useState(false) - const [winnerResults, setWinnerResults] = useState | null>(null) + const [saved, setSaved] = useState(null) + const [aiLoading, setAiLoading] = useState(null) // step being AI'd + const [showTiming, setShowTiming] = useState(false) const editorRef = useRef(null) @@ -92,118 +64,70 @@ export default function AutomationsPage() { if (data.config) setConfig(data.config) if (data.channels) setChannels(data.channels) if (data.stats) setStats(data.stats) - if (data.history) setHistory(data.history) - if (data.pending) setPending(data.pending) } catch { /* */ } setLoading(false) }, []) useEffect(() => { load() }, [load]) - // Sync editor when selection changes - useEffect(() => { - const tpl = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === activeVariant) - if (tpl) { - setEditBody(tpl.body); setEditSubject(tpl.subject || ""); setEditName(tpl.name) - setDirty(false) - } else { - setEditBody(""); setEditSubject(""); setEditName(STEP_META[activeStep]?.label || "") - setDirty(false) - } - setAiReasoning(null); setAiStrategy(null) - }, [activeStep, activeChannel, activeVariant, templates]) + // 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) - const tplA = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === "A") - const tplB = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === "B") - const hasAB = !!tplB - const currentTpl = templates.find(t => t.step === activeStep && t.channel === activeChannel && t.variant === activeVariant) + const startEdit = (step: number) => { + const t = tpl(step) + if (t) { setEditBody(t.body); setEditing(step) } + setTimeout(() => editorRef.current?.focus(), 50) + } - const activeChannels = [ - channels?.whatsapp ? "whatsapp" : null, - channels?.email ? "email" : null, - channels?.sms ? "sms" : null, - ].filter(Boolean) as Channel[] + const cancelEdit = () => { setEditing(null); setEditBody("") } - // ─── Actions ────────────────────────────── - - const saveTemplate = async () => { + const saveEdit = async (step: number) => { setSaving(true) + const t = tpl(step) try { await fetch("/api/automations", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ templates: [{ step: activeStep, channel: activeChannel, variant: activeVariant, name: editName, subject: activeChannel === "email" ? editSubject : null, body: editBody, splitPercent: hasAB ? 50 : 100 }] }), + body: JSON.stringify({ templates: [{ step, channel: t?.channel || "whatsapp", variant: "A", name: t?.name || "Message", body: editBody }] }), }) - setSaved(true); setDirty(false) - setTimeout(() => setSaved(false), 2000) + setSaved(step); setEditing(null) + setTimeout(() => setSaved(null), 2000) await load() } catch { /* */ } setSaving(false) } - const aiGenerateVariant = async () => { - setAiGenerating(true); setAiReasoning(null); setAiStrategy(null) + 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: activeStep, channel: activeChannel }), + body: JSON.stringify({ action: "generate_variant", step, channel: "whatsapp" }), }) const data = await res.json() - if (data.ok && data.variant) { - setAiReasoning(data.variant.reasoning) - setAiStrategy(data.variant.strategy) - setActiveVariant("B") - await load() - } + if (data.ok) await load() } catch { /* */ } - setAiGenerating(false) + setAiLoading(null) } - const aiRewrite = async (instruction: string) => { - setRewriting(instruction) - try { - const res = await fetch("/api/automations/ai", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "rewrite", step: activeStep, channel: activeChannel, variant: activeVariant, instruction, currentBody: editBody }), - }) - const data = await res.json() - if (data.ok && data.body) { - setEditBody(data.body); setDirty(true) - } - } catch { /* */ } - setRewriting(null) - } - - const checkWinners = async () => { - setCheckingWinners(true); setWinnerResults(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" }), }) - const data = await res.json() - if (data.results) { setWinnerResults(data.results); await load() } + if (res.ok) await load() } catch { /* */ } - setCheckingWinners(false) + setAiLoading(null) } - const deleteVariantB = async () => { - setSaving(true) + const removeVariant = async (step: number) => { try { await fetch("/api/automations", { method: "DELETE", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ step: activeStep, channel: activeChannel, variant: "B" }), - }) - setActiveVariant("A"); setAiReasoning(null); setAiStrategy(null) - await load() - } catch { /* */ } - setSaving(false) - } - - const saveStrategy = async (strategy: string, matrix: Record) => { - try { - await fetch("/api/automations", { - method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ config: { strategy, channelMatrix: matrix } }), + body: JSON.stringify({ step, channel: "whatsapp", variant: "B" }), }) await load() } catch { /* */ } @@ -219,589 +143,252 @@ export default function AutomationsPage() { } catch { /* */ } } - const insertVariable = (key: string) => { - const el = editorRef.current - if (!el) return - const start = el.selectionStart; const end = el.selectionEnd - const insert = `{{${key}}}` - setEditBody(editBody.slice(0, start) + insert + editBody.slice(end)) - setDirty(true) - setTimeout(() => { el.focus(); el.setSelectionRange(start + insert.length, start + insert.length) }, 0) - } - - // ─── Render ─────────────────────────────── - if (loading) return
- // Count A/B tests across all steps - const abCount = new Set(templates.filter(t => t.variant === "B").map(t => `${t.step}:${t.channel}`)).size + 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 ── */} -
-
-

Message design studio

-

Automations

-
-
- {abCount > 0 && ( - - )} - {stats && stats.total > 0 && ( -
-

{stats.total}

-

this week · {stats.deliveryRate}%

-
- )} -
+ {/* Header — one line */} +
+

What your donors receive

+

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

- {/* ── Winner Results Banner ── */} - {winnerResults && winnerResults.some(r => r.action === "promoted") && ( -
-

Winners promoted

- {winnerResults.filter(r => r.action === "promoted").map((r, i) => ( -
- {STEP_META[r.step]?.label} · {CH_META[r.channel as Channel]?.label} - → Variant {r.winner} wins ({r.winnerRate}% vs {r.loserRate}%) - {r.newChallenger && New AI challenger created} -
- ))} - -
- )} - {winnerResults && !winnerResults.some(r => r.action === "promoted") && ( -
-

Not enough data yet

-

Each variant needs at least 20 sends before we can pick a winner. Keep collecting pledges!

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

WhatsApp not connected. These messages will 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 +

)} - {/* ── No channels warning ── */} - {activeChannels.length === 0 && ( -
- -
-

No channels connected yet

-

Design your messages now — they'll start sending once you connect WhatsApp in Settings.

-
-
- )} - - {/* ── Step Timeline ── */} -
- {STEP_META.map((s, i) => { - const isActive = activeStep === i - const delay = i === 0 ? "Instant" : i === 1 ? `Day ${config?.step1Delay || 2}` : i === 2 ? `Day ${config?.step2Delay || 7}` : `Day ${config?.step3Delay || 14}` - const hasABForStep = templates.some(t => t.step === i && t.variant === "B") - return ( - - ) - })} -
- - {/* ── Main: Phone + Editor ── */} -
- - {/* ── LEFT: Phone Mockup ── */} -
- - {activeStep > 0 && ( -
- Send - - after pledge (if not paid) -
- )} - - {/* A/B stats comparison (below phone) */} - {hasAB && tplA && tplB && (tplA.sentCount > 0 || tplB.sentCount > 0) && ( - - )} -
- - {/* ── RIGHT: Editor ── */} -
- - {/* Channel tabs */} -
- {CHANNELS.map(ch => { - const m = CH_META[ch]; const isAct = activeChannel === ch; const isLive = activeChannels.includes(ch); const Icon = m.icon - return ( - - ) - })} -
- - {/* A/B variant toggle — AI-native */} -
- - - {hasAB ? ( - <> - - - - ) : ( - - )} -
- - {/* AI reasoning banner */} - {aiReasoning && ( -
-
- - - {aiStrategy?.replace(/_/g, " ") || "AI Strategy"} - -
-

{aiReasoning}

-
- )} - - {/* Template name */} - { setEditName(e.target.value); setDirty(true) }} - className="text-lg font-black text-[#111827] bg-transparent border-b-2 border-transparent hover:border-gray-200 focus:border-[#1E40AF] outline-none w-full pb-1 transition-colors" - placeholder="Template name..." /> - - {/* Email subject */} - {activeChannel === "email" && ( -
- - { setEditSubject(e.target.value); setDirty(true) }} - className="w-full h-10 px-3 border-2 border-gray-200 text-sm font-medium placeholder:text-gray-300 focus:border-[#1E40AF] outline-none" placeholder="e.g. Your £{{amount}} pledge — payment details" /> -

Preview: {resolvePreview(editSubject || "")}

-
- )} - - {/* AI rewrite toolbar */} -
-

AI rewrite

-
- {REWRITE_ACTIONS.map(a => ( - - ))} -
-
- - {/* Message body editor */} -
-
- - {editBody.length} chars{activeChannel === "sms" ? ` · ${Math.ceil(editBody.length / 160)} SMS` : ""} -
-