Email Engine ships (L3): sequence generation (welcome/follow-up/re-engagement/broadcast) on the Suite engine with tolerant EMAIL/SUBJECT/BODY parsing, per-email copy buttons, metered; tile live
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Email Engine — write follow-up sequences, welcome series and broadcasts in the team voice, with your invite link."><title>Email Engine | The Circle Suite</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">
|
||||
<style>
|
||||
.ew{max-width:880px;margin:0 auto;padding:24px 20px 70px}
|
||||
.ew h1{font-size:clamp(26px,4.5vw,36px);margin:6px 0 8px}
|
||||
.ew .lede{color:var(--muted);line-height:1.6;margin-bottom:18px}
|
||||
#emGate{display:none;border:2px solid var(--gold);border-radius:12px;padding:16px 20px;margin-bottom:18px;line-height:1.6}
|
||||
#emGate b{color:var(--gold)}
|
||||
.em-card{background:var(--card,#0d2236);border:1px solid var(--line,#24425d);border-radius:14px;padding:18px 20px}
|
||||
.em-kinds{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
|
||||
.em-kind{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;
|
||||
padding:7px 14px;font-size:13.5px;font-weight:700;cursor:pointer}
|
||||
.em-kind.on{border-color:var(--teal);background:rgba(78,214,203,.12);color:var(--teal)}
|
||||
label.em-lab{display:block;font-weight:700;font-size:14px;margin:0 0 6px}
|
||||
#emBrief{width:100%;min-height:88px;resize:vertical;background:var(--bg,#0b1d2e);border:1px solid var(--line);
|
||||
border-radius:10px;color:var(--text);padding:11px 13px;font-size:15px;font-family:inherit;line-height:1.5}
|
||||
.em-hint{color:var(--muted);font-size:12.5px;margin-top:6px}
|
||||
.em-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:14px}
|
||||
#emMeter{font-size:12.5px;color:var(--muted);margin-left:auto}
|
||||
#emMeter b{color:var(--text)}
|
||||
#emErr{display:none;color:#f0a05a;margin-top:12px;font-size:14px;line-height:1.55}
|
||||
#emOut{display:none;margin-top:18px}
|
||||
.em-mail{background:var(--bg,#0b1d2e);border:1px solid var(--line);border-left:3px solid var(--gold);
|
||||
border-radius:10px;padding:14px 16px;margin-bottom:12px}
|
||||
.em-mail .n{font-size:11px;font-weight:800;letter-spacing:.12em;color:var(--muted);text-transform:uppercase}
|
||||
.em-mail .s{font-weight:800;font-size:16px;margin:4px 0 8px;color:var(--text)}
|
||||
.em-mail .b{white-space:pre-wrap;font-size:15px;line-height:1.6;color:var(--muted)}
|
||||
.em-mail .act{margin-top:10px}
|
||||
.em-note{margin-top:20px;font-size:12.5px;color:var(--muted);line-height:1.6;border-top:1px solid var(--line);padding-top:12px}
|
||||
.spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--gold);
|
||||
border-radius:50%;animation:sp .8s linear infinite;vertical-align:-2px;margin-right:7px}
|
||||
@keyframes sp{to{transform:rotate(360deg)}}
|
||||
@media (prefers-reduced-motion:reduce){.spin{animation:none}}
|
||||
</style></head>
|
||||
<body>
|
||||
<header class="wrap nav"><a class="brand" href="/suite"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>Email Engine</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/suite">← The Suite</a><a class="btn btn-primary" href="/suite/copy">Copy Engine</a></div></header>
|
||||
<main class="ew">
|
||||
<div class="eyebrow" style="color:var(--gold)">CIRCLE SUITE · LEVEL 3 · FABRICA</div>
|
||||
<h1>Email <span class="gold">Engine.</span></h1>
|
||||
<p class="lede">Whole sequences, not one-offs. Pick the job, say who the list is, and it writes the series in the team's voice with <strong style="color:var(--text)">your</strong> link — ready to paste into any autoresponder.</p>
|
||||
|
||||
<div id="emGate"></div>
|
||||
|
||||
<div class="em-card" id="emCard">
|
||||
<label class="em-lab">What do you need?</label>
|
||||
<div class="em-kinds" id="emKinds"></div>
|
||||
<label class="em-lab" for="emBrief">Who's the list, and what should it emphasise?</label>
|
||||
<textarea id="emBrief" placeholder="e.g. People from my marketing list who know me but have never touched crypto — lead with how simple it is and be upfront about risk"></textarea>
|
||||
<div class="em-hint">Mention anything you want included: your story, their situation, objections you keep hearing.</div>
|
||||
<div class="em-row">
|
||||
<button id="emGo" class="btn btn-primary">✉️ Write the sequence</button>
|
||||
<span id="emMeter"></span>
|
||||
</div>
|
||||
<div id="emErr"></div>
|
||||
</div>
|
||||
|
||||
<div id="emOut"></div>
|
||||
|
||||
<p class="em-note"><strong style="color:var(--text)">Teach it forward:</strong> a member with a follow-up sequence running never loses a warm prospect to silence — show your two how to set theirs up. <br>Read every email before you send it; your name is on it. Generated copy follows the team's rules — no income promises, no invented numbers, honest about risk. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
|
||||
</main>
|
||||
<script src="/suite-email.js"></script>
|
||||
<script src="/chat.js" defer></script>
|
||||
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
|
||||
@@ -0,0 +1,124 @@
|
||||
// Email Engine client — sequence picker, brief, then a card per email with
|
||||
// its own copy button (people paste them one at a time into an autoresponder).
|
||||
(function () {
|
||||
'use strict';
|
||||
var KINDS = [
|
||||
{ k: 'welcome', label: 'Welcome series (4)' },
|
||||
{ k: 'followup', label: 'Follow-up series (4)' },
|
||||
{ k: 'reengage', label: 'Re-engagement (3)' },
|
||||
{ k: 'broadcast', label: 'Single broadcast' }
|
||||
];
|
||||
var PLACEHOLDERS = {
|
||||
welcome: "e.g. New members who joined from my warm market — most are brand new to crypto and a little nervous",
|
||||
followup: "e.g. People who watched my video and said they'd think about it — no pressure, keep it useful",
|
||||
reengage: "e.g. Members who joined a month ago, got their wallet set up, then went quiet",
|
||||
broadcast: "e.g. My marketing list — they know me from my content, not from crypto. Lead with the toolkit angle."
|
||||
};
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
var kind = 'welcome', busy = false;
|
||||
|
||||
function renderKinds() {
|
||||
var host = $('emKinds');
|
||||
host.innerHTML = '';
|
||||
KINDS.forEach(function (x) {
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'em-kind' + (x.k === kind ? ' on' : '');
|
||||
b.textContent = x.label;
|
||||
b.addEventListener('click', function () {
|
||||
kind = x.k;
|
||||
$('emBrief').placeholder = PLACEHOLDERS[kind] || '';
|
||||
renderKinds();
|
||||
});
|
||||
host.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function showMeter(m) {
|
||||
if (!m || !m.limit) { $('emMeter').textContent = ''; return; }
|
||||
$('emMeter').innerHTML = '<b>' + m.remaining + '</b> of ' + m.limit + ' emails left this month';
|
||||
}
|
||||
|
||||
function gate(msg) {
|
||||
var g = $('emGate');
|
||||
g.style.display = 'block';
|
||||
g.innerHTML = msg;
|
||||
$('emCard').style.opacity = '.55';
|
||||
$('emGo').disabled = true;
|
||||
$('emBrief').disabled = true;
|
||||
}
|
||||
|
||||
function renderEmails(list) {
|
||||
var out = $('emOut');
|
||||
out.innerHTML = '';
|
||||
list.forEach(function (e, i) {
|
||||
var d = document.createElement('div');
|
||||
d.className = 'em-mail';
|
||||
d.innerHTML = '<div class="n">Email ' + (i + 1) + '</div><div class="s"></div><div class="b"></div>' +
|
||||
'<div class="act"><button class="btn btn-teal btn-sm">📋 Copy this email</button> <span class="em-hint ok" style="display:none;color:var(--teal)">Copied.</span></div>';
|
||||
d.querySelector('.s').textContent = e.subject;
|
||||
d.querySelector('.b').textContent = e.body;
|
||||
d.querySelector('button').addEventListener('click', function () {
|
||||
navigator.clipboard.writeText('Subject: ' + e.subject + '\n\n' + e.body).then(function () {
|
||||
var ok = d.querySelector('.ok');
|
||||
ok.style.display = '';
|
||||
setTimeout(function () { ok.style.display = 'none'; }, 2200);
|
||||
});
|
||||
});
|
||||
out.appendChild(d);
|
||||
});
|
||||
out.style.display = 'block';
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
renderKinds();
|
||||
try {
|
||||
var r = await fetch('/api/public/suite-meters');
|
||||
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.'); return; }
|
||||
if (r.status === 403) { gate('The Email Engine isn’t open for this position yet. <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.'); return; }
|
||||
if (!r.ok) return;
|
||||
var d = await r.json();
|
||||
var m = d.meters && d.meters.email;
|
||||
if (m && m.reason === 'locked') {
|
||||
gate('The Email Engine unlocks at <b>Fabrica (level 3)</b>. You’re at level ' + d.level + ' — keep climbing and it opens, along with the Video Maker. <a href="/how-pay-works#exact-costs" style="color:var(--gold);font-weight:700">See what upgrades cost →</a>');
|
||||
return;
|
||||
}
|
||||
showMeter(m);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
$('emErr').style.display = 'none';
|
||||
$('emGo').disabled = true;
|
||||
$('emGo').innerHTML = '<span class="spin"></span>Writing the sequence… (up to two minutes)';
|
||||
try {
|
||||
var r = await fetch('/api/public/suite-email', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ kind: kind, brief: $('emBrief').value.trim() })
|
||||
});
|
||||
var d = await r.json();
|
||||
if (!r.ok) {
|
||||
$('emErr').textContent = d.error || 'That didn’t go through — try again.';
|
||||
$('emErr').style.display = 'block';
|
||||
if (d.meter) showMeter(d.meter);
|
||||
} else {
|
||||
renderEmails(d.emails || []);
|
||||
showMeter(d.meter);
|
||||
$('emOut').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
} catch (e) {
|
||||
$('emErr').textContent = 'Connection hiccup — try again.';
|
||||
$('emErr').style.display = 'block';
|
||||
}
|
||||
busy = false;
|
||||
$('emGo').disabled = false;
|
||||
$('emGo').textContent = '✉️ Write the sequence';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
boot();
|
||||
$('emGo').addEventListener('click', run);
|
||||
});
|
||||
})();
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
{ lv: 1, ico: '🤖', name: 'AI Coach', desc: '24/7 answers about the system, in 21+ languages — the chat bubble on every page of this site.', href: '#', live: true, coach: true },
|
||||
{ lv: 2, ico: '✍️', name: 'Copy Engine', desc: 'An AI copywriter tuned to this business: posts, DMs, follow-ups and objection replies with your link filled in.', href: '/suite/copy', live: true },
|
||||
{ lv: 2, ico: '🧱', name: 'Page Builder', desc: 'Answer four questions, get your own hosted invitation page — your story, your angle video, your QR.', href: '/suite/page', live: true },
|
||||
{ lv: 3, ico: '📧', name: 'Email Engine', desc: 'Follow-up sequences and broadcasts in the team voice, exportable to any autoresponder.', live: false },
|
||||
{ lv: 3, ico: '📧', name: 'Email Engine', desc: 'Welcome series, follow-up sequences and broadcasts in the team voice — exportable to any autoresponder.', href: '/suite/email', live: true },
|
||||
{ lv: 3, ico: '🎥', name: 'Video Maker', desc: 'The team’s master promo videos rendered with your personal end-card — your name, your QR, your link.', live: false },
|
||||
{ lv: 4, ico: '🗣️', name: 'Voice Profile + Funnels', desc: 'Output that sounds like YOU, plus multi-page funnels on your own team subdomain.', live: false },
|
||||
{ lv: 5, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising: monthly ad credits on the team’s own network, banner and text placements, rotator priority, and AI campaign packs.', live: false },
|
||||
|
||||
@@ -21,6 +21,7 @@ const suiteMeter = require('./suite-meter'); suiteMeter.init({ dataDir: DATA_DIR
|
||||
const suiteAI = require('./suite-ai'); suiteAI.init({ dataDir: DATA_DIR });
|
||||
const suitePages = require('./suite-pages'); suitePages.init({ dataDir: DATA_DIR });
|
||||
const suiteTools = require('./suite-tools');
|
||||
const suiteEmail = require('./suite-email');
|
||||
const tgbot = require('./tgbot');
|
||||
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
|
||||
const SESSION_TTL = 8 * 60 * 60 * 1000;
|
||||
@@ -704,6 +705,29 @@ async function handleApi(req,res,pathname){
|
||||
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
|
||||
}
|
||||
|
||||
if(req.method==='POST'&&pathname==='/api/public/suite-email'){
|
||||
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500}));
|
||||
if(e.error)return json(res,e.code||500,{error:e.error});
|
||||
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
|
||||
const b=await bodyJson(req)||{};
|
||||
const kind=String(b.kind||'welcome');
|
||||
const brief=String(b.brief||'').trim().slice(0,1200);
|
||||
if(!suiteEmail.KINDS[kind])return json(res,400,{error:'Unknown sequence type.'});
|
||||
if(!suiteAI.configured())return json(res,503,{error:'The Email Engine is warming up — try again shortly.'});
|
||||
const gate=suiteMeter.check(e.d.id,e.d.level,'email');
|
||||
if(!gate.allowed){
|
||||
return json(res,429,{error:gate.reason==='locked'
|
||||
? 'The Email Engine unlocks at Fabrica (level 3). Your next upgrades open it.'
|
||||
: 'You have used all '+gate.limit+' email generations this month. It resets on the 1st — or a level upgrade raises your allowance.',meter:gate});
|
||||
}
|
||||
try{
|
||||
const link='https://rmcircle.team/join/'+e.d.id;
|
||||
const emails=await suiteEmail.generate(kind,brief,{link:link,id:e.d.id});
|
||||
suiteMeter.record(e.d.id,'email',emails.length||1);
|
||||
return json(res,200,{emails:emails,meter:suiteMeter.check(e.d.id,e.d.level,'email')});
|
||||
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
|
||||
}
|
||||
|
||||
if(req.method==='GET'&&pathname==='/api/public/suite-meters'){
|
||||
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup.',code:500}));
|
||||
if(e.error)return json(res,e.code||500,{error:e.error});
|
||||
@@ -1037,7 +1061,7 @@ const server=http.createServer(async(req,res)=>{
|
||||
if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]);
|
||||
}
|
||||
let file;
|
||||
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{
|
||||
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{
|
||||
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';
|
||||
}
|
||||
if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Circle Suite — Email Engine (L3). Same engine, same compliance layer as the
|
||||
// Copy Engine; the difference is shape: it writes SEQUENCES, not single pieces.
|
||||
//
|
||||
// Output contract is the same trick that works elsewhere: labelled plain text
|
||||
// parsed deterministically, with a forgiving parser (models drop colons and
|
||||
// add markdown emphasis).
|
||||
'use strict';
|
||||
const suiteAI = require('./suite-ai');
|
||||
|
||||
const KINDS = {
|
||||
welcome: {
|
||||
label: 'Welcome sequence',
|
||||
count: 4,
|
||||
brief: 'a 4-email welcome sequence for someone who just joined the team under this member. Email 1 welcomes them and points at the first 48 hours. Email 2 covers getting their first two. Email 3 handles the doubt that shows up in week one. Email 4 is about teaching their two to do the same.'
|
||||
},
|
||||
followup: {
|
||||
label: 'Follow-up sequence',
|
||||
count: 4,
|
||||
brief: 'a 4-email follow-up sequence for someone who looked at the opportunity but has not joined. Space the emails a few days apart in tone. No pressure, no fake deadlines, no guilt. Each email should be useful on its own and easy to ignore.'
|
||||
},
|
||||
reengage: {
|
||||
label: 'Re-engagement sequence',
|
||||
count: 3,
|
||||
brief: 'a 3-email re-engagement sequence for a member who joined but went quiet. Warm, no shaming, assume life got busy. Make the next step small and specific.'
|
||||
},
|
||||
broadcast: {
|
||||
label: 'Single broadcast',
|
||||
count: 1,
|
||||
brief: 'ONE standalone broadcast email to a list about the team build.'
|
||||
}
|
||||
};
|
||||
|
||||
function buildPrompt(kind, brief, member) {
|
||||
const k = KINDS[kind] || KINDS.broadcast;
|
||||
const link = (member && member.link) || '';
|
||||
return (
|
||||
'Write ' + k.brief + '\n\n' +
|
||||
'CONTEXT FROM THE MEMBER (who the list is, what to emphasise):\n' +
|
||||
(brief || '(nothing specific — write something honest and general)') + '\n\n' +
|
||||
'Return EXACTLY this shape, repeated ' + k.count + ' time(s), and nothing else:\n' +
|
||||
'EMAIL 1\n' +
|
||||
'SUBJECT: one line, under 60 characters, no clickbait, no ALL CAPS\n' +
|
||||
'BODY: 90-170 words, plain text, short paragraphs separated by blank lines, ending with a sign-off line\n' +
|
||||
(k.count > 1 ? 'EMAIL 2\nSUBJECT: ...\nBODY: ...\n(and so on)\n' : '') + '\n' +
|
||||
(link ? 'Where a link belongs, use exactly: ' + link + '\n' : 'Do not invent a link.\n') +
|
||||
'No HTML, no markdown headers, no emoji in subject lines, no merge tags.'
|
||||
);
|
||||
}
|
||||
|
||||
// Tolerant parse: EMAIL n / SUBJECT / BODY, colon and emphasis optional.
|
||||
function parseEmails(raw) {
|
||||
const text = String(raw || '').replace(/\r/g, '');
|
||||
const marks = [];
|
||||
const re = /(?:^|\n)[ \t]*[*#>\s]*EMAIL[ \t]*#?\s*(\d+)[ \t]*[*#]*[ \t]*:?[ \t]*/gi;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) marks.push({ n: Number(m[1]), at: m.index, from: m.index + m[0].length });
|
||||
const chunks = [];
|
||||
if (!marks.length) {
|
||||
chunks.push(text);
|
||||
} else {
|
||||
marks.forEach(function (mk, i) {
|
||||
chunks.push(text.slice(mk.from, i + 1 < marks.length ? marks[i + 1].at : text.length));
|
||||
});
|
||||
}
|
||||
const out = [];
|
||||
chunks.forEach(function (c) {
|
||||
const sm = c.match(/(?:^|\n)[ \t]*[*#>\s]*SUBJECT[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*(.*)/i);
|
||||
const bm = c.match(/(?:^|\n)[ \t]*[*#>\s]*BODY[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*([\s\S]*)/i);
|
||||
const subject = sm ? sm[1].replace(/[*#]+/g, '').trim().slice(0, 120) : '';
|
||||
let body = bm ? bm[1] : (sm ? c.slice(c.indexOf(sm[0]) + sm[0].length) : c);
|
||||
body = String(body).replace(/^[*#\s]+/, '').trim();
|
||||
if (subject || body) out.push({ subject: subject || '(no subject)', body: body });
|
||||
});
|
||||
return out.filter(function (e) { return e.body && e.body.length > 20; });
|
||||
}
|
||||
|
||||
async function generate(kind, brief, member) {
|
||||
const raw = await suiteAI.generateRaw(buildPrompt(kind, brief, member));
|
||||
let emails = parseEmails(raw);
|
||||
if (!emails.length) throw new Error('The writer came back incomplete — try again.');
|
||||
const link = member && member.link;
|
||||
if (link) {
|
||||
emails = emails.map(function (e) {
|
||||
let b = e.body.replace(/https?:\/\/(?:www\.)?rmcircle\.team\/\S*/gi, link);
|
||||
b = b.replace(/\[LINK\]|\{link\}/gi, link);
|
||||
return { subject: e.subject, body: b };
|
||||
});
|
||||
}
|
||||
return emails;
|
||||
}
|
||||
|
||||
module.exports = { KINDS, generate, parseEmails };
|
||||
Reference in New Issue
Block a user