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:
186
pledge-now-pay-later/src/lib/ai.ts
Normal file
186
pledge-now-pay-later/src/lib/ai.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user