feat: premium UI overhaul, AI suggestions, WAHA WhatsApp integration

PREMIUM UI:
- All animations: fade-up, scale-in, stagger children, confetti celebration
- Glass effects, gradient icons, premium card hover states
- Custom CSS: shimmer, pulse-ring, bounce, counter-roll animations
- Smooth progress bar with gradient

AI-POWERED (GPT-4o-mini nano model):
- Smart amount suggestions based on peer data (/api/ai/suggest)
- Social proof: '42 people pledged · Average £85'
- AI-generated nudge text for conversion
- AI fuzzy matching for bank reconciliation
- AI reminder message generation

WAHA WHATSAPP INTEGRATION:
- Auto-send pledge receipt with bank details via WhatsApp
- 4-step reminder sequence: gentle → nudge → urgent → final
- Chatbot: donors reply PAID, HELP, CANCEL, STATUS
- Volunteer notification on new pledges
- WhatsApp status in dashboard settings
- Webhook endpoint for incoming messages

DONOR FLOW (CRO):
- Amount step: AI suggestions, Gift Aid preview, social proof, haptic feedback
- Payment step: trust signals, fee comparison, benefit badges
- Identity step: email/phone toggle, WhatsApp reminder indicator
- Bank instructions: tap-to-copy each field, WhatsApp delivery confirmation
- Confirmation: confetti, pulse animation, share CTA, WhatsApp receipt

COMPOSE:
- Added WAHA env vars + qc-comms network for WhatsApp access
This commit is contained in:
2026-03-03 04:31:07 +08:00
parent 0236867c88
commit c6e7e4f01e
15 changed files with 1473 additions and 383 deletions

View File

@@ -0,0 +1,186 @@
/**
* AI module — uses OpenAI GPT-4o-mini (nano model, ~$0.15/1M input tokens)
* Falls back to smart heuristics when no API key is set
*/
const OPENAI_KEY = process.env.OPENAI_API_KEY
const MODEL = "gpt-4o-mini"
interface ChatMessage {
role: "system" | "user" | "assistant"
content: string
}
async function chat(messages: ChatMessage[], maxTokens = 300): Promise<string> {
if (!OPENAI_KEY) return ""
try {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${OPENAI_KEY}`,
},
body: JSON.stringify({ model: MODEL, messages, max_tokens: maxTokens, temperature: 0.7 }),
})
const data = await res.json()
return data.choices?.[0]?.message?.content || ""
} catch {
return ""
}
}
/**
* Generate smart amount suggestions based on event context + peer data
*/
export async function suggestAmounts(context: {
eventName: string
avgPledge?: number
medianPledge?: number
pledgeCount?: number
topAmount?: number
currency?: string
}): Promise<{ amounts: number[]; nudge: string; socialProof: string }> {
const avg = context.avgPledge || 5000 // 50 quid default
const median = context.medianPledge || avg
// Smart anchoring: show amounts around the median, biased upward
const base = Math.round(median / 1000) * 1000 || 5000
const amounts = [
Math.max(1000, Math.round(base * 0.5 / 500) * 500),
base,
Math.round(base * 2 / 1000) * 1000,
Math.round(base * 5 / 1000) * 1000,
Math.round(base * 10 / 1000) * 1000,
Math.round(base * 20 / 1000) * 1000,
].filter((v, i, a) => a.indexOf(v) === i && v >= 500) // dedup, min £5
// Social proof text
let socialProof = ""
if (context.pledgeCount && context.pledgeCount > 3) {
socialProof = `${context.pledgeCount} people have pledged so far`
if (context.avgPledge) {
socialProof += ` · Average £${Math.round(context.avgPledge / 100)}`
}
}
// AI-generated nudge (or fallback)
let nudge = ""
if (OPENAI_KEY && context.pledgeCount && context.pledgeCount > 5) {
nudge = await chat([
{
role: "system",
content: "You write one short, warm, encouraging line (max 12 words) to nudge a charity donor to pledge generously. UK English. No emojis. No pressure.",
},
{
role: "user",
content: `Event: ${context.eventName}. ${context.pledgeCount} people pledged avg £${Math.round((context.avgPledge || 5000) / 100)}. Generate a nudge.`,
},
], 30)
}
if (!nudge) {
const nudges = [
"Every pound makes a real difference",
"Your generosity changes lives",
"Join others making an impact today",
"Be part of something meaningful",
]
nudge = nudges[Math.floor(Math.random() * nudges.length)]
}
return { amounts: amounts.slice(0, 6), nudge, socialProof }
}
/**
* Generate a personalized thank-you / reminder message
*/
export async function generateMessage(type: "thank_you" | "reminder_gentle" | "reminder_urgent" | "whatsapp_receipt", vars: {
donorName?: string
amount: string
eventName: string
reference: string
orgName?: string
daysSincePledge?: number
}): Promise<string> {
const name = vars.donorName?.split(" ")[0] || "there"
// Templates with AI enhancement
const templates: Record<string, string> = {
thank_you: `Thank you${name !== "there" ? `, ${name}` : ""}! Your £${vars.amount} pledge to ${vars.eventName} means the world. Ref: ${vars.reference}`,
reminder_gentle: `Hi ${name}, just a friendly nudge about your £${vars.amount} pledge to ${vars.eventName}. If you've already paid — thank you! Ref: ${vars.reference}`,
reminder_urgent: `Hi ${name}, your £${vars.amount} pledge to ${vars.eventName} is still pending after ${vars.daysSincePledge || "a few"} days. We'd love to close this out — every penny counts. Ref: ${vars.reference}`,
whatsapp_receipt: `🤲 *Pledge Confirmed!*\n\n💷 Amount: £${vars.amount}\n📋 Event: ${vars.eventName}\n🔖 Reference: \`${vars.reference}\`\n\n${name !== "there" ? `Thank you, ${name}!` : "Thank you!"} Your generosity makes a real difference.\n\n_Powered by Pledge Now, Pay Later_`,
}
let msg = templates[type] || templates.thank_you
// Try AI-enhanced version for reminders
if (OPENAI_KEY && (type === "reminder_gentle" || type === "reminder_urgent")) {
const aiMsg = await chat([
{
role: "system",
content: `You write short, warm ${type === "reminder_urgent" ? "but firm" : "and gentle"} payment reminder messages for a UK charity. Max 3 sentences. Include the reference number. UK English. Be human, not corporate.`,
},
{
role: "user",
content: `Donor: ${name}. Amount: £${vars.amount}. Event: ${vars.eventName}. Reference: ${vars.reference}. Days since pledge: ${vars.daysSincePledge || "?"}. Generate the message.`,
},
], 100)
if (aiMsg) msg = aiMsg
}
return msg
}
/**
* AI-powered fuzzy reference matching for bank reconciliation
*/
export async function smartMatch(bankDescription: string, candidates: Array<{ ref: string; amount: number; donor: string }>): Promise<{
matchedRef: string | null
confidence: number
reasoning: string
}> {
if (!OPENAI_KEY || candidates.length === 0) {
return { matchedRef: null, confidence: 0, reasoning: "No AI key or no candidates" }
}
const candidateList = candidates.map(c => `${c.ref}${(c.amount / 100).toFixed(2)}, ${c.donor || "anonymous"})`).join(", ")
const result = await chat([
{
role: "system",
content: 'You match bank transaction descriptions to pledge references. Return ONLY valid JSON: {"ref":"MATCHED_REF","confidence":0.0-1.0,"reasoning":"short reason"}. If no match, ref should be null.',
},
{
role: "user",
content: `Bank description: "${bankDescription}"\nPossible pledge refs: ${candidateList}\n\nWhich pledge reference does this bank transaction match?`,
},
], 80)
try {
const parsed = JSON.parse(result)
return {
matchedRef: parsed.ref || null,
confidence: parsed.confidence || 0,
reasoning: parsed.reasoning || "",
}
} catch {
return { matchedRef: null, confidence: 0, reasoning: "Parse error" }
}
}
/**
* Generate event description from a simple prompt
*/
export async function generateEventDescription(prompt: string): Promise<string> {
if (!OPENAI_KEY) return ""
return chat([
{
role: "system",
content: "You write concise, compelling charity event descriptions for a UK audience. Max 2 sentences. Warm and inviting.",
},
{ role: "user", content: prompt },
], 60)
}