diff --git a/public/suite-email.html b/public/suite-email.html new file mode 100644 index 0000000..4a52205 --- /dev/null +++ b/public/suite-email.html @@ -0,0 +1,62 @@ +Email Engine | The Circle Suite + + + +
+
CIRCLE SUITE · LEVEL 3 · FABRICA
+

Email Engine.

+

Whole sequences, not one-offs. Pick the job, say who the list is, and it writes the series in the team's voice with your link — ready to paste into any autoresponder.

+ +
+ +
+ +
+ + +
Mention anything you want included: your story, their situation, objections you keep hearing.
+
+ + +
+
+
+ +
+ +

Teach it forward: a member with a follow-up sequence running never loses a warm prospect to silence — show your two how to set theirs up.
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.

+
+ + + diff --git a/public/suite-email.js b/public/suite-email.js new file mode 100644 index 0000000..844704a --- /dev/null +++ b/public/suite-email.js @@ -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 = '' + m.remaining + ' 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 = '
Email ' + (i + 1) + '
' + + '
'; + 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. Open the Suite 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. See your Suite.'); 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 Fabrica (level 3). You’re at level ' + d.level + ' — keep climbing and it opens, along with the Video Maker. See what upgrades cost →'); + return; + } + showMeter(m); + } catch (e) {} + } + + async function run() { + if (busy) return; + busy = true; + $('emErr').style.display = 'none'; + $('emGo').disabled = true; + $('emGo').innerHTML = '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); + }); +})(); diff --git a/public/suite.js b/public/suite.js index 2c27ff3..5ec18d6 100644 --- a/public/suite.js +++ b/public/suite.js @@ -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 }, diff --git a/server.js b/server.js index 3ec2e8a..c55e4d9 100644 --- a/server.js +++ b/server.js @@ -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); diff --git a/suite-email.js b/suite-email.js new file mode 100644 index 0000000..ab60790 --- /dev/null +++ b/suite-email.js @@ -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 };