// Split Tester client. Reuses the text-ad writer to produce candidates, then // launches the chosen ones as one test through the Traffic Desk. (function () { 'use strict'; var $ = function (id) { return document.getElementById(id); }; function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function n(v) { return Number(v || 0).toLocaleString(); } var state = { angle: 'general', angles: [], variants: [], picked: [], target: 'join', remaining: 0, limit: 0, hasPage: false, minPerArm: 500, maxArms: 3, id: null }; var busy = false; function chip(label, on, fn) { var b = document.createElement('button'); b.type = 'button'; b.className = 's-chip' + (on ? ' on' : ''); b.textContent = label; b.addEventListener('click', fn); return b; } function gate(msg) { $('sGate').style.display = 'block'; $('sGate').innerHTML = msg; $('sCard').style.opacity = '.55'; $('sGo').disabled = true; $('sWrite').disabled = true; } function renderAngles() { var host = $('sAngles'); host.innerHTML = ''; state.angles.forEach(function (a) { host.appendChild(chip(a.label, state.angle === a.key, function () { state.angle = a.key; renderAngles(); })); }); } function renderTargets() { var host = $('sTargets'); host.innerHTML = ''; host.appendChild(chip('My invite page', state.target === 'join', function () { state.target = 'join'; renderTargets(); })); if (state.hasPage) { host.appendChild(chip('My personal page', state.target === 'page', function () { state.target = 'page'; renderTargets(); })); } } function renderVariants() { var host = $('sVariants'); host.innerHTML = ''; if (!state.variants.length) return; var grid = document.createElement('div'); grid.className = 's-ads'; state.variants.forEach(function (v, i) { var pickIdx = state.picked.indexOf(i); var card = document.createElement('div'); card.className = 's-ad' + (pickIdx !== -1 ? ' on' : ''); if (pickIdx !== -1) { var tag = document.createElement('span'); tag.className = 'tag'; tag.textContent = String.fromCharCode(65 + pickIdx); card.appendChild(tag); } var s = document.createElement('div'); s.className = 's'; s.textContent = v.subject; card.appendChild(s); v.lines.forEach(function (l) { if (!l) return; var d = document.createElement('div'); d.className = 'l'; d.textContent = l; card.appendChild(d); }); card.addEventListener('click', function () { var at = state.picked.indexOf(i); if (at !== -1) state.picked.splice(at, 1); else if (state.picked.length < state.maxArms) state.picked.push(i); renderVariants(); splitHint(); }); grid.appendChild(card); }); host.appendChild(grid); var hint = document.createElement('div'); hint.className = 's-hint'; hint.textContent = 'Tap 2 or 3 of these to put them head to head. Tap again to unpick.'; host.appendChild(hint); } function splitHint() { var arms = state.picked.length; var total = Number($('sImp').value) || 0; var el = $('sSplitHint'); if (arms < 2) { el.textContent = 'Pick at least two versions above first.'; return; } var per = Math.floor(total / arms); var need = state.minPerArm * arms; el.innerHTML = arms + ' versions × ' + n(per) + ' impressions each. ' + (per < state.minPerArm ? 'Too thin to mean anything — use at least ' + n(need) + ' total for ' + arms + ' versions.' : 'Comes out of this month\'s allowance; stopping the test returns whatever has not served.'); } async function writeAds(btn) { btn.disabled = true; btn.innerHTML = 'Writing…'; $('sErr').style.display = 'none'; try { var r = await fetch('/api/public/suite-textads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ angle: state.angle }) }); var d = await r.json(); if (!r.ok) { $('sErr').textContent = d.error || 'The writer could not produce versions just now.'; $('sErr').style.display = 'block'; } else { state.variants = d.variants || []; state.picked = state.variants.length >= 2 ? [0, 1] : []; renderVariants(); splitHint(); if (d.meter) $('sWriteMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' batches left this month'; } } catch (e) { $('sErr').textContent = 'Connection hiccup — try again.'; $('sErr').style.display = 'block'; } btn.disabled = false; btn.innerHTML = state.variants.length ? '✍️ Write different versions' : '✍️ Write me some versions'; } function renderTests(tests) { if (!tests || !tests.length) { $('sTestsWrap').style.display = 'none'; return; } $('sTestsWrap').style.display = 'block'; var host = $('sTests'); host.innerHTML = ''; tests.forEach(function (t) { var box = document.createElement('div'); box.className = 's-test'; var when = String(t.at || '').slice(0, 10); var best = (t.verdict && t.verdict.winner) || null; var rows = (t.armResults || []).map(function (a) { return '' + '' + esc(a.arm) + '' + '' + esc(a.subject) + '
' + (a.lines || []).filter(Boolean).map(esc).join(' · ') + '' + '' + n(a.served) + '' + '' + n(a.clicks) + '' + '' + (a.served > 0 ? (a.rate * 100).toFixed(3) + '%' : '—') + ''; }).join(''); box.innerHTML = '

Test from ' + esc(when) + ' — ' + n(t.total) + ' impressions across ' + (t.arms || []).length + ' versions' + (t.stopped ? ' (stopped, ' + n(t.refunded) + ' returned)' : '') + '

' + '
' + esc((t.verdict && t.verdict.text) || '') + '
' + '
' + rows + '
VersionServedClicksClick rate
'; if (!t.stopped) { var btn = document.createElement('button'); btn.className = 'btn btn-secondary btn-sm'; btn.style.marginTop = '10px'; btn.textContent = 'Stop this test'; btn.title = 'Stops every version and returns the unserved impressions'; btn.addEventListener('click', function () { stopTest(t.testId, btn); }); box.appendChild(btn); } host.appendChild(box); }); } async function stopTest(testId, btn) { btn.disabled = true; btn.textContent = 'Stopping…'; try { var r = await fetch('/api/public/suite-split', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stop: testId }) }); var d = await r.json(); if (!r.ok) { $('sErr').textContent = d.error || 'Could not stop that test.'; $('sErr').style.display = 'block'; btn.disabled = false; btn.textContent = 'Stop this test'; return; } $('sOk').innerHTML = '✅ Test stopped. ' + n(d.stopped.refunded) + ' unserved impressions went back into your balance.'; $('sOk').style.display = 'block'; boot(); } catch (e) { btn.disabled = false; btn.textContent = 'Stop this test'; } } function apply(d) { state.id = d.id; state.hasPage = !!d.hasPage; state.minPerArm = d.minPerArm || 500; state.maxArms = d.maxArms || 3; state.angles = d.textAngles || state.angles; if (!state.hasPage && state.target === 'page') state.target = 'join'; var tr = d.traffic || {}; state.remaining = tr.remaining || 0; state.limit = tr.limit || 0; $('sBal').style.display = 'flex'; $('sRemain').textContent = n(state.remaining); $('sLimit').textContent = n(state.limit); $('sImp').max = state.remaining; if (Number($('sImp').value) > state.remaining) $('sImp').value = state.remaining; if (d.textMeter) $('sWriteMeter').textContent = d.textMeter.remaining + ' of ' + d.textMeter.limit + ' batches left this month'; renderAngles(); renderTargets(); renderVariants(); splitHint(); renderTests(d.tests || []); if (state.remaining < state.minPerArm * 2) { $('sGo').disabled = true; $('sGo').textContent = 'Not enough impressions left this month'; } } async function run() { if (busy) return; $('sErr').style.display = 'none'; $('sOk').style.display = 'none'; if (state.picked.length < 2) { $('sErr').textContent = 'Pick at least two versions to compare.'; $('sErr').style.display = 'block'; return; } busy = true; $('sGo').disabled = true; $('sGo').innerHTML = 'Launching every version…'; try { var arms = state.picked.map(function (i) { return { subject: state.variants[i].subject, lines: state.variants[i].lines }; }); var r = await fetch('/api/public/suite-split', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ arms: arms, impressions: Number($('sImp').value) || 0, target: state.target, angle: state.angle }) }); var d = await r.json(); if (!r.ok) { $('sErr').textContent = d.error || 'That did not go through — try again.'; $('sErr').style.display = 'block'; } else { $('sOk').innerHTML = '✅ Test running. ' + d.test.arms.length + ' versions, ' + n(d.test.perArm) + ' impressions each. Check back in a few days — display clicks take a while to accumulate.'; $('sOk').style.display = 'block'; setTimeout(boot, 800); } } catch (e) { $('sErr').textContent = 'Connection hiccup — try again.'; $('sErr').style.display = 'block'; } busy = false; $('sGo').disabled = false; $('sGo').textContent = '🔬 Start the test'; } async function boot() { try { var r = await fetch('/api/public/suite-split'); 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) { var g = await r.json(); gate((g.error || 'Not open for this position yet.') + ' See your Suite.'); return; } if (!r.ok) return; apply(await r.json()); } catch (e) {} } document.addEventListener('DOMContentLoaded', function () { boot(); $('sGo').addEventListener('click', run); $('sWrite').addEventListener('click', function () { writeAds(this); }); $('sImp').addEventListener('input', splitHint); }); })();