1/* ========================================================================== 2 outreach-autonomy.js â THE AUTONOMY LADDER (2026-09-29) 3 4 Built to the prototype Alex approved on 28 Sep 5 (docs/projects/outreach-review/ladder/prototype.html). Two screens: 6 7 1. The onboarding step before LinkedIn (#page-outreach-ladder): the 8 prototype's "Before anything goes out in your name" screen. Community, 9 who to reach and first sources are done; this is where a new coach gives 10 permission, then connects. Opened by ui-core.js startLinkedInConnect() 11 while /auth/me says outreach_needs_autonomy. 12 2. The panel at the top of the Outreach dashboard (#od-autonomy): the 13 prototype's dark ladder card, three rungs, "You are here", tap to 14 preview, NEW / BACK TO YOU, Move up / Step back. Since o9 (2026-09-29) 15 it is ONE line once a step is chosen, and Change opens it in place 16 (lineHtml / panelView); a preview no longer lists what stays the same. 17 18 Alex's answers (28 Sep) change the prototype in these places, on purpose: 19 no marks to earn (the coach picks freely), no automatic step-down (the panel 20 tells them instead), Full = up to 8 comments a day (its own stepper since 21 o9, O13; 8 until the coach changes it), connection requests 22 go out with the note approved once on every step, no "start paused" and no 23 Start button after connecting, the tone chips became comment styles chosen 24 by goal, and the step shows real example comments and the English Maya 25 writes in. 26 27 Server: /api/outreach-customer/autonomy (services/outreach/autonomy.js). 28 ========================================================================== */ 29(function () { 30 'use strict'; 31 32 var RUNGS = ['manual', 'semi', 'full']; 33 var R = { 34 manual: { n: 1, name: 'Manual', line: 'You approve every comment. Invitations use the note you approved.', 35 head: 'Every comment waits for you.', 36 lead: 'Maya finds people, reads their profiles and likes their posts, and connection requests go out with the note you approved. Nothing is commented in your name until you press Approve.' }, 37 semi: { n: 2, name: 'Semi', line: 'Comments within your limit go out. You see them after.', 38 head: 'Comments within your daily limit and your style go out without you.', 39 lead: 'You see each one after, with the reason it went out and an undo until it posts. Anything unusual still waits for you.' }, 40 full: { n: 3, name: 'Full', line: 'It runs. You handle what is unusual.', 41 head: 'Outreach runs on its own, inside your limits.', 42 lead: 'Comments within your daily limit go out without you. You are only asked when something is unusual.' } 43 }; 44 45 function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } 46 function idx(r) { return RUNGS.indexOf(r); } 47 function initials(name) { 48 var p = String(name || '').trim().split(/\s+/).filter(Boolean); 49 return ((p[0] || '?').charAt(0) + (p.length > 1 ? p[p.length - 1].charAt(0) : '')).toUpperCase(); 50 } 51 function capOf(st, r) { return r === 'full' ? (st.full_cap || 8) : (st.semi_cap || 3); } 52 /* "1 comment a day goes", "3 comments a day go" (o9, O15). */ 53 function perDay(n, verb) { 54 n = Number(n) || 0; 55 return n + (n === 1 ? ' comment a day' : ' comments a day') + (verb ? (n === 1 ? ' goes' : ' go') : ''); 56 } 57 function styleName(st, id) { 58 var s = (st.styles || []).filter(function (x) { return x.id === id; })[0]; 59 return s ? s.name : null; 60 } 61 var toastT; 62 function toast(msg) { 63 var t = document.getElementById('oal-toast'); 64 if (!t) { t = document.createElement('div'); t.id = 'oal-toast'; t.setAttribute('role', 'status'); document.body.appendChild(t); } 65 t.textContent = msg; t.classList.add('on'); 66 clearTimeout(toastT); toastT = setTimeout(function () { t.classList.remove('on'); }, 3600); 67 } 68 function call(path, opts) { 69 if (typeof window.api !== 'function' && typeof api !== 'function') return Promise.resolve(null); 70 var f = typeof window.api === 'function' ? window.api : api; 71 return Promise.resolve(f(path, opts || {})).catch(function () { return null; }); 72 } 73 74 /* ââ How people from outreach join, in words (round 5, job 14) âââââââââ */ 75 var JOIN = { 76 open: { short: 'Straight in', line: 'People who accept your invitation join your community straight in.' }, 77 approval: { short: 'You approve each one', line: 'People who accept your invitation ask to join, and wait in Requests on your Members page until you let them in.' } 78 }; 79 function joinMode(settings) {
80 var oc = settings && settings.owner_controls; 81 if (!oc) return null; 82 return oc.outreach_member_admission === 'approval' ? 'approval' : 'open'; 83 } 84 85 /* ââ Who decides each thing (the prototype's matrix, Alex's defaults) ââ */ 86 function matrixRows(st) { 87 var semiCap = capOf(st, 'semi'), fullCap = capOf(st, 'full'); 88 var sn = styleName(st, st.voice && st.voice.style); 89 return [ 90 { a: 'Find people who fit and read their profiles', v: { manual: ['maya', 'Maya, up to 80 a day. You can remove anyone.'], semi: ['maya', 'Maya, up to 80 a day. You can remove anyone.'], full: ['maya', 'Maya, up to 80 a day. You can remove anyone.'] } }, 91 { a: 'Like one recent post', v: { manual: ['maya', 'Maya, up to 5 a day.'], semi: ['maya', 'Maya, up to 5 a day.'], full: ['maya', 'Maya, up to 5 a day.'] } }, 92 { a: 'Comment on their post', v: { manual: ['you', 'You approve each one.'], 93 semi: ['maya', 'Maya, up to ' + semiCap + ' a day' + (sn ? ', as ' + sn.toLowerCase() : '') + '. Sensitive topics wait.'], 94 full: ['maya', 'Maya, up to ' + fullCap + ' a day' + (sn ? ', as ' + sn.toLowerCase() : '') + '. Sensitive topics wait.'] } }, 95 { a: 'Connection request with your note', v: { manual: ['maya', 'Maya, with the note you approved.'], semi: ['maya', 'Maya, with the note you approved.'], full: ['maya', 'Maya, with the note you approved.'] } }, 96 { a: 'Welcome email when someone accepts', v: { manual: ['maya', 'Maya sends your approved version, or our standard welcome until you approve one.'], semi: ['maya', 'Maya sends your approved version, or our standard welcome until you approve one.'], full: ['maya', 'Maya sends your approved version, or our standard welcome until you approve one.'] } }, 97 { a: 'LinkedIn messages', v: { manual: ['off', 'Only if you allow them.'], semi: ['off', 'Only if you allow them.'], full: ['off', 'Only if you allow them.'] } } 98 ]; 99 } 100 101 function diffFor(from, to, st) { 102 var semiCap = capOf(st, 'semi'), fullCap = capOf(st, 'full'); 103 var D = { 104 'manual>semi': [['plus', 'Comments within ' + semiCap + ' a day, in your style, go out without you. You see each one after, with an undo until it posts.'], 105 ['same', 'Connection requests go out with the note you approved, as now.'], 106 ['same', 'Anything unusual still waits: sensitive topics, and anything strongly worded.'], 107 ['same', 'One tap takes you back to Manual.']], 108 'manual>full': [['plus', 'Comments, up to ' + fullCap + ' a day, go out without you. You see each one after, with an undo until it posts.'], 109 ['same', 'Anything unusual still waits for you.'], 110 ['same', 'One tap takes you back.']], 111 'semi>full': [['plus', 'Up to ' + perDay(fullCap) + ' instead of ' + semiCap + '.'], 112 ['same', 'Sensitive topics still wait for you. One tap takes you back to Semi.']], 113 'semi>manual': [['minus', 'Every comment waits for you again. Anything Maya queued that has not posted comes back to your list.'], 114 ['same', 'Likes, profile reading, connection requests and your welcome email carry on.']], 115 'full>semi': [['minus', 'Comments drop back to ' + semiCap + ' a day.'], 116 ['same', 'Unusual things still wait for you.']], 117 'full>manual': [['minus', 'Every comment waits for you again.'], 118 ['minus', 'Anything Maya queued that has not posted comes back to your list.']], 119 'unset>manual': [['same', 'Every comment waits for your Approve, as it does now.'], 120 ['same', 'Connection requests carry on with your note, as now.']], 121 'unset>semi': [['plus', 'Comments within ' + semiCap + ' a day, in your style, go out without you. You see each one after, with an undo until it posts.'], 122 ['same', 'Anything unusual still waits: sensitive topics, and anything strongly worded.'], 123 ['same', 'One tap takes you back to Manual.']], 124 'unset>full': [['plus', 'Comments, up to ' + fullCap + ' a day, go out without you. You see each one after, with an undo until it posts.'], 125 ['same', 'Anything unusual still waits for you. One tap takes you back.']] 126 }; 127 /* A preview says only what changes (o9, Grace: a "SAME / SAME" box filled a 128 whole card to say nothing changes). The lines that stay the same are 129 left out; a move that changes nothing says so in one line (diffHtml). */ 130 return (D[from + '>' + to] || []).filter(function (d) { return d[0] !== 'same'; }).map(function (d) { 131 return '<li><span class="k ' + d[0] + '">' + (d[0] === 'plus' ? 'NEW' : 'BACK TO YOU') + '</span><span>' + esc(d[1]) + '</span></li>'; 132 }).join(''); 133 } 134 function diffHtml(from, to, st) { 135 var li = diffFor(from, to, st); 136 if (li) return '<ul class="diff">' + li + '</ul>'; 137 return '<p class="nochange" data-oal-nochange>Nothing changes: ' + (to === 'manual' ? 'every comment already waits for your Approve.' : 'this is how it works today.') + '</p>'; 138 } 139 140 function langLine(st, compact) { 141 var v = st.voice || {}; 142 var det = v.detected || {}; 143 var sp = v.spelling || det.spelling || 'us'; 144 var name = sp === 'uk' ? 'UK English' : 'US English'; 145 var other = sp === 'uk' ? 'us' : 'uk'; 146 var lead; 147 if (v.spelling) lead = 'Maya writes in <b>' + name + '</b>.'; 148 else if (det.basis === 'writing') lead = 'Weâve detected you write in <b>' + name + '</b>.'; 149 else if (det.basis === 'country' || det.basis === 'timezone') lead = 'Youâre based where people write <b>' + name + '</b>, so Maya does too.'; 150 else if (det.basis === 'currency') lead = 'Your business works in a country that writes <b>' + name + '</b>, so Maya does too.'; 151 else if (det.basis === 'targets') lead = 'The people you reach write <b>' + name + '</b>, so Maya does too.'; 152 else lead = 'Maya writes in <b>' + name + '</b>.'; 153 return '<div class="lang"' + (compact ? ' style="margin-top:12px"' : '') + '><span>' + lead + '</span>' + 154 '<button class="linkish" data-oal="spelling" data-v="' + other + '">Change to ' + (other === 'uk' ? 'UK' : 'US') + ' English</button></div>'; 155 } 156 157 /* Round 7 (r1 F-7): the style drafts follow is always shown as chosen; a 158 coach who never picked one is told it is the default. */ 159 function styleNote(st) { 160 var v = st.voice || {}; 161 if (v.style_saved !== false) return ''; 162 var n = styleName(st, v.style) || 'Warm encouragement'; 163 return '<div class="card-meta" style="margin-top:6px" data-oal-style-note>' + esc(n) + ' until you pick another. Every draft follows the one picked.</div>'; 164 } 165 function limitsHtml(st, level) { 166 var style = st.voice && st.voice.style; 167 /* Full has the same stepper as Semi (o9, O13), up to 8 a day. */ 168 var cap = level === 'full' 169 ? '<div class="limit"><div class="t">Comments a day without me</div><div class="stepper"><button data-oal="fcap" data-v="-1" aria-label="One fewer"' + (capOf(st, 'full') <= 1 ? ' disabled' : '') + '>−</button><span class="v">' + capOf(st, 'full') + '</span><button data-oal="fcap" data-v="1" aria-label="One more"' + (capOf(st, 'full') >= 8 ? ' disabled' : '') + '>+</button><span class="card-meta">Up to 8 on Full</span></div></div>' 170 : '<div class="limit"><div class="t">Comments a day without me</div><div class="stepper"><button data-oal="cap" data-v="-1" aria-label="One fewer"' + (capOf(st, 'semi') <= 1 ? ' disabled' : '') + '>−</button><span class="v">' + capOf(st, 'semi') + '</span><button data-oal="cap" data-v="1" aria-label="One more"' + (capOf(st, 'semi') >= 8 ? ' disabled' : '') + '>+</button></div></div>'; 171 return '<div class="limits">' + cap + 172 '<div class="limit"><div class="t">My comment style</div><div class="chips">' + (st.styles || []).map(function (s) { 173 return '<button class="chip' + (style === s.id ? ' on' : '') + '" data-oal="style" data-v="' + esc(s.id) + '" title="' + esc(s.purpose) + '">' + esc(s.name) + '</button>'; 174 }).join('') + '</div>' + styleNote(st) + '</div>' + 175 '</div>'; 176 } 177 178 /* ââ Sensitive topics (services/outreach/sensitive-topics.js) âââââââââ 179 Two tiers: ALWAYS wait (no switch), and wait UNLESS the coach switches 180 one off. The coach's own field comes first, with the reason. Shown in the 181 onboarding step's "Never, on any step" card and on the dashboard panel. */
182 function topicsHtml(st, opts) { 183 var t = st.topics; 184 if (!t || !t.always) return ''; 185 var o = opts || {}; 186 var h = '<div class="topics" data-oal-topics>' + 187 (o.heading === false ? '' : '<div class="sect-h">Sensitive topics</div>') + 188 '<p class="tp-lead">Maya never comments on these on her own. Before she posts anything herself she checks what the post is about, not just its words, and a draft that touches one of these waits for you, on every step, and says why.' + 189 (o.manual ? ' On Manual every comment waits anyway; these apply when you move up.' : '') + '</p>' + 190 '<div class="tp-h">Always wait for you</div><div class="chips">' + 191 t.always.map(function (x) { return '<span class="chip fixed" title="' + esc(x.line) + '">' + esc(x.name) + '</span>'; }).join('') + '</div>' + 192 '<div class="tp-h" style="margin-top:14px">Wait for you unless you switch them off</div>'; 193 (t.adjustable || []).forEach(function (x) { 194 h += '<div class="tp-row' + (x.relevant ? ' rel' : '') + '"><div class="tp-b"><b>' + esc(x.name) + '</b><span>' + esc(x.line) + '</span>' + 195 (x.niche_line ? '<span class="tp-niche">' + esc(x.niche_line) + '</span>' : '') + '</div>' + 196 '<button type="button" class="tp-sw' + (x.held ? ' on' : '') + '" role="switch" aria-checked="' + (x.held ? 'true' : 'false') + '" aria-label="' + esc(x.name) + ' waits for you" data-oal="topic" data-v="' + esc(x.id) + '">' + 197 '<span class="knob" aria-hidden="true"></span><span class="tp-st">' + (x.held ? 'Waits for you' : 'Maya may comment') + '</span></button></div>'; 198 }); 199 return h + '</div>'; 200 } 201 function saveTopic(stRef, id, repaint) { 202 var t = stRef.topics && (stRef.topics.adjustable || []).filter(function (x) { return x.id === id; })[0]; 203 if (!t) return; 204 var held = !t.held; 205 call('/outreach-customer/autonomy/topics', { method: 'POST', body: { topic: id, held: held } }).then(function (r) { 206 if (!r || !r.ok) { toast(r && r.error === 'always_held' ? 'That one always waits for you.' : 'That did not save. Nothing changed; try again.'); return; } 207 stRef.topics = r.topics; repaint(); 208 toast(held ? t.name + ' waits for you again.' : 'Maya may comment on ' + t.name.toLowerCase() + ' now, within your daily limit. Everything else still waits.'); 209 }); 210 } 211 212 /* Example drafts: the drafts waiting in Comments for these posts (round 7, 213 r1 F-2), or written exactly as they are, each saying if Semi would hold it. */ 214 function exampleItemsHtml(ex) { 215 var e = ex || {}; 216 if (e.loading) return '<div class="ex"><div class="rd">Maya is writing a few examples from your own discoveryâ¦</div></div>'; 217 if (!(e.items || []).length) return '<div class="ex"><div class="rd">' + (e.error ? 'Maya could not write examples just now. Try again in a minute.' : 'Maya has not found posts to comment on yet. Your first examples appear here as soon as discovery finds some.') + '</div></div>'; 218 return e.items.map(function (it) { 219 var words = it.words || String(it.draft || '').trim().split(/\s+/).filter(Boolean).length; 220 return '<div class="ex"><div class="nm">' + esc(it.author) + '</div><div class="post">' + esc(String(it.post_text).slice(0, 280)) + (String(it.post_text).length > 280 ? 'â¦' : '') + '</div>' + 221 '<div class="draft"><div class="lab">Mayaâs draft, in your name · ' + words + ' words' + (it.waiting ? ' · waiting in Comments now' : '') + '</div>' + esc(it.draft) + '</div>' + 222 (it.held_reason ? '<div class="exheld">On Semi or Full this one waits for you. ' + esc(it.held_reason) + '</div>' : '') + '</div>'; 223 }).join(''); 224 } 225 226 /* The dashboard's "Tune how Maya comments" (round 6, r1 F-3 and r2 F6): the 227 examples and the framing box, for every account, not only on the 228 onboarding step an existing account never sees. */ 229 function tuneHtml(st, ui) { 230 var open = !!ui.tune; 231 var h = '<div class="tune"><button type="button" class="tune-toggle" data-oal="tune" aria-expanded="' + (open ? 'true' : 'false') + '">' + 232 '<span>Tune how Maya comments</span><span class="card-meta">Examples, and what to tell her</span><span class="car" aria-hidden="true">' + (open ? 'â´' : 'â¾') + '</span></button>'; 233 if (!open) return h + '</div>'; 234 var chosen = st.level === 'semi' || st.level === 'full'; 235 h += '<div class="tune-body">' + 236 '<div class="sect-h">How your comments will sound</div><div class="tune-ex">' + exampleItemsHtml(ui.examples) + '</div>'; 237 if (!chosen) { 238 var style = st.voice && st.voice.style; 239 h += '<div class="sect-h" style="margin-top:14px">My comment style</div><div class="chips">' + (st.styles || []).map(function (x) { 240 return '<button class="chip' + (style === x.id ? ' on' : '') + '" data-oal="style" data-v="' + esc(x.id) + '" title="' + esc(x.purpose) + '">' + esc(x.name) + '</button>'; 241 }).join('') + '</div>' + styleNote(st); 242 } 243 h += '<div class="sect-h" style="margin-top:14px">Tell Maya how to frame your comments</div>' + 244 '<textarea class="oal-ta" id="oal-p-direction" placeholder="For example: never mention my programme, and use first names.">' + esc(ui.direction != null ? ui.direction : ((st.voice && st.voice.direction) || '')) + '</textarea>' + 245 '<div class="actions" style="margin-top:8px"><button class="btn btn-sm" data-oal="retryEx"' + (ui.examples && ui.examples.loading ? ' disabled' : '') + '>Save and write the examples again</button>' + 246 '<span class="card-meta">' + (ui.examples && ui.examples.limit_reached ? 'That is todayâs examples; your choices still apply to every comment.' : 'Your style, English and notes apply to every comment Maya drafts.') + '</span></div>' + 247 '</div></div>'; 248 return h; 249 } 250 251 /* The last 7 days, grouped by day in the coach's own calendar (o9, O12: 252 "Went out without you" covered today only, so a week away could not be 253 reviewed in one place). */ 254 function dayKey(t) { var d = new Date(t); if (isNaN(d.getTime())) return ''; return d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate(); } 255 function dayLabel(t, nowMs) { 256 var d = new Date(t); if (isNaN(d.getTime())) return 'Earlier'; 257 var days = Math.round((_dayStart(nowMs == null ? Date.now() : nowMs) - _dayStart(d.getTime())) / 86400000); 258 if (days <= 0) return 'Today'; 259 if (days === 1) return 'Yesterday'; 260 /* The one date system (date-format.js), as fmtTime uses it. */ 261 try { 262 if (typeof App !== 'undefined' && App.Dates && typeof App.Dates.weekdayShort === 'function') return App.Dates.weekdayShort(d); 263 } catch (_) {} 264 return days + ' days ago'; 265 } 266 function wentOutHtml(st) { 267 var list = st.went_out || []; 268 var h = '<div class="sect-h" style="margin-top:18px">Went out without you this week <span class="count">' + list.length + '</span></div>'; 269 if (!list.length) { 270 return h + '<div class="empty">' + (st.linkedin_connected === false 271 ? 'Nothing yet: Maya queues comments once your LinkedIn is connected.' 272 : (st.paused ? 'Nothing: your outreach is paused.' : 'Nothing in the last 7 days. Maya checks for new posts every hour.')) + 273 heldLine(st.held, false) + '</div>'; 274 } 275 var lastDay = null; 276 list.forEach(function (s) { 277 var k = s.at ? dayKey(s.at) : 'x'; 278 if (k !== lastDay) { h += '<div class="wo-day" data-oal-day>' + esc(s.at ? dayLabel(s.at) : 'Earlier') + '</div>'; lastDay = k; } 279 var stLabel = s.state === 'posted' ? 'Posted' : s.state === 'undone' ? 'Taken back' : s.state === 'stopped' ? 'Not posted' : 'Queued'; 280 var when = s.state === 'queued' && s.planned_at ? 'Posts about ' + fmtTime(s.planned_at) : (s.state === 'posted' && s.sent_at ? 'Posted ' + fmtTime(s.sent_at) : ''); 281 h += '<div class="item"><div class="item-h"><div class="avatar">' + esc(initials(s.member_name)) + '</div>' + 282 '<div class="who"><div class="nm">Comment on ' + esc(s.member_name) + 'âs post</div><div class="rl">' + esc(when) + '</div></div>' + 283 '<span class="st ' + esc(s.state) + '">' + stLabel + '</span></div>' + 284 (s.post_text ? '<div class="post">' + esc(String(s.post_text).slice(0, 280)) + (String(s.post_text).length > 280 ? 'â¦' : '') + '</div>' : '') + 285 '<div class="draft"><div class="lab">What goes out, in your name</div>' + esc(s.text) + '</div>' + 286 (s.why && s.why.length ? '<div class="why"><span class="w">Why it went out</span>' + s.why.map(function (x) { return '<span class="c">' + esc(x) + '</span>'; }).join('') + '</div>' : ''); 287 if (s.state === 'queued' && s.outbound_id) { 288 h += '<div class="actions"><button class="btn btn-sm" data-oal="undo" data-v="' + esc(s.outbound_id) + '">Undo, it waits for me</button><span class="card-meta">You can undo until it posts.</span></div>'; 289 } else if (s.state === 'posted') { 290 h += '<div class="cant">Posted on LinkedIn. We cannot take a comment back; you can delete it on LinkedIn.</div>'; 291 } 292 h += '</div>'; 293 }); 294 if (st.held) h += '<div class="empty">' + heldLine(st.held, true) + '</div>'; 295 return h; 296 } 297 /* ââ On each waiting draft (the Comments page, subpages.js) ââââââââââââ 298 When it expires (round 6, r1 F-4, r2 F3): a draft expires 5 days after WE 299 found the post (services/outreach/comment-expiry.js), so the card says 300 when we found it and when it goes, in the coach's own calendar days. */ 301 var EXPIRE_DAYS = 5; 302 function _dayStart(ms) { var d = new Date(ms); d.setHours(0, 0, 0, 0); return d.getTime(); } 303 function expiryLine(foundIso, nowMs) { 304 var f = new Date(foundIso).getTime(); 305 if (!isFinite(f)) return null; 306 var now = nowMs == null ? Date.now() : nowMs; 307 var exp = f + EXPIRE_DAYS * 86400000; 308 var foundDays = Math.round((_dayStart(now) - _dayStart(f)) / 86400000); 309 var found = foundDays <= 0 ? 'Found today' : foundDays === 1 ? 'Found yesterday' : 'Found ' + foundDays + ' days ago';
310 if (exp <= now) return { found: found, expires: 'Expired', soon: true, expired: true }; 311 var d = Math.round((_dayStart(exp) - _dayStart(now)) / 86400000); 312 var hr = new Date(exp).getHours(); 313 var expires = d <= 0 ? (hr >= 17 ? 'Expires tonight' : 'Expires today') : d === 1 ? 'Expires tomorrow' : 'Expires in ' + d + ' days'; 314 return { found: found, expires: expires, soon: d <= 1, expired: false }; 315 } 316 /* What the step will do with this draft (GET /autonomy/plan). */ 317 function planLineHtml(item) { 318 if (!item || !item.line) return ''; 319 var kind = item.verdict === 'post' ? 'go' : 'wait'; 320 return '<div class="cm-plan ' + kind + '" data-comment-plan="' + esc(item.verdict) + '"><b>' + esc(item.line) + '</b>' + 321 (item.reason ? '<span>' + esc(item.reason) + '</span>' : '') + '</div>'; 322 } 323 324 /* "1 draft waiting for you is held back", "3 drafts ⦠are" (round 6, r1 F-6). */ 325 function heldLine(n, more) { 326 n = Number(n) || 0; 327 if (!n) return ''; 328 return (more ? '' : ' ') + n + (more ? ' more' : '') + (n === 1 ? ' draft waiting for you is' : ' drafts waiting for you are') + 329 ' held back: a sensitive topic, strong words, not checked yet, or over todayâs limit. Comments shows why on each one.'; 330 } 331 function fmtTime(iso) { 332 /* The one date system (date-format.js), in the tenant's saved order. */ 333 try { 334 var d = new Date(iso); if (isNaN(d.getTime())) return ''; 335 if (typeof App !== 'undefined' && App.Dates && typeof App.Dates.weekdayShortTime === 'function') return App.Dates.weekdayShortTime(d); 336 return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 337 } catch (_) { return ''; } 338 } 339 340 /* ââ 2. THE DASHBOARD PANEL (prototype: ladderHtml) âââââââââââââââââââ */ 341 function panelHtml(st, ui) { 342 var chosen = st.level === 'manual' || st.level === 'semi' || st.level === 'full'; 343 var cur = chosen ? st.level : null; 344 var pv = ui.preview || cur || 'manual'; 345 var pr = cur ? RUNGS[idx(cur) - 1] : null; 346 var nx = cur ? RUNGS[idx(cur) + 1] : null; 347 var h2, lead; 348 if (!chosen) { 349 h2 = 'Choose how much Maya does on her own.'; 350 lead = st.learned_acting 351 ? 'Today, comments that match the rules you switched on can go out, up to ' + (st.cap || 0) + ' a day. Everything else waits for you. Pick a step and it replaces that.' 352 : 'Today, every comment waits for you, as on Manual. Nothing changes until you pick a step.'; 353 } else if (st.paused) { 354 h2 = 'Paused on ' + R[cur].name + '. Nothing is going out.'; 355 lead = 'When you resume, it carries on exactly as ' + R[cur].name + ' says below.'; 356 } else if (st.linkedin_connected === false) { 357 /* One truth with the dashboard header's "Not running yet" (round 6, 358 r1 F-1): the step is chosen, and nothing runs until LinkedIn is in. */ 359 h2 = 'On ' + R[cur].name + '. Nothing goes out until LinkedIn is connected.'; 360 lead = 'Once it is, ' + R[cur].name + ' works as below. Comments shows what ' + R[cur].name + ' would do with each draft waiting now.'; 361 } else { 362 h2 = 'You are on ' + R[cur].name + '. ' + R[cur].head; 363 lead = cur === 'semi' 364 ? 'You see each one after, with the reason it went out and an undo until it posts. Anything unusual still waits for you.' 365 : R[cur].lead; 366 } 367 var h = '<section class="ladder" aria-label="How much Maya does on her own"><div class="eyebrow">How much Maya does on her own</div>' + 368 '<h2>' + esc(h2) + '</h2><p class="lead">' + esc(lead) + '</p>' + 369 '<div class="rungs">' + RUNGS.map(function (r) { 370 var cls = 'rung' + (r === cur ? ' here' : '') + (ui.preview === r || (!chosen && pv === r) ? ' previewing' : ''); 371 return '<button class="' + cls + '" data-oal="preview" data-v="' + r + '"' + (r === cur ? ' aria-current="step"' : '') + '><div class="num">Step ' + R[r].n + '</div><div class="nm">' + R[r].name + '</div><div class="ln">' + esc(R[r].line) + '</div>' + 372 (r === cur ? '<span class="pin">You are here</span>' 373 /* Never chosen (round 7, r2 m3): today works exactly like Manual, so it says so. */ 374 : (!chosen && r === 'manual' && !st.learned_acting) ? '<span class="pin" data-oal-today>You are here today</span>' 375 : (cur && idx(r) < idx(cur) ? '<span class="done">Tap to step back</span>' : '')) + '</button>'; 376 }).join('') + '</div>' + 377 '<div class="ladder-foot"><span class="hint">Tap a step to see exactly what would change. Nothing changes until you press a button.</span>' + 378 (pr ? '<button class="btn btn-onDark btn-sm" data-oal="move" data-v="' + pr + '">Step back to ' + R[pr].name + '</button>' : '') + 379 /* Opened from the one line (o9): Close folds it back to that line. */ 380 (chosen && ui.open ? '<button class="btn btn-onDark btn-sm" data-oal="close" aria-expanded="true">Close</button>' : '') + '</div></section>'; 381 382 var offline = st.linkedin_connected === false; 383 h += '<div class="card"><div class="card-head"><div class="card-title">' + esc(cur && pv === cur ? (offline ? 'How ' + R[cur].name + ' will run' : 'On ' + R[cur].name + ' right now') : (chosen ? 'If you move to ' : 'If you choose ') + R[pv].name) + '</div>' + 384 (cur && pv !== cur ? '<button class="btn btn-quiet btn-sm" data-oal="preview" data-v="' + cur + '">Back to where I am</button>' 385 : '<span class="card-meta">' + (cur ? 'You can change this at any time' : 'Nothing has changed yet') + '</span>') + '</div><div class="card-body">'; 386 if (st.advice && st.advice.text && cur && pv === cur) h += '<div class="advice">' + esc(st.advice.text) + '</div>'; 387 388 if (cur && pv === cur) { 389 var runs = [], waits = []; 390 matrixRows(st).forEach(function (row) { 391 var v = row.v[cur]; 392 if (v[0] === 'maya') runs.push('<b>' + esc(row.a) + '.</b> ' + esc(v[1])); 393 else if (v[0] === 'you') waits.push('<b>' + esc(row.a) + '.</b> ' + esc(v[1])); 394 else waits.push('<b>' + esc(row.a) + '.</b> ' + esc(v[1])); 395 }); 396 if (ui.join) runs.push('<b>People who accept.</b> ' + esc(JOIN[ui.join].line) + ' <button class="linkish" data-oal="join">Change</button>'); 397 if (cur !== 'manual') waits.push('<b>Anything unusual.</b> Sensitive topics, strongly worded drafts, and anything over todayâs limit' + (st.held ? ' (' + st.held + ' waiting now)' : '') + '.'); 398 h += '<div class="detail-cols"><div><div class="col-h runs"><span class="sq"></span>' + (offline ? 'Will run in your name once LinkedIn connects' : 'Runs in your name') + '</div><ul class="plain r">' + runs.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul></div>' + 399 '<div><div class="col-h waits"><span class="sq"></span>Waits for you</div><ul class="plain w">' + waits.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul></div></div>'; 400 if (cur !== 'manual') h += limitsHtml(st, cur); 401 h += langLine(st, true); 402 h += topicsHtml(st, { manual: cur === 'manual' }); 403 h += tuneHtml(st, ui); 404 if (cur !== 'manual') h += wentOutHtml(st); 405 if (nx) { 406 h += '<div class="next"><h3>Next step: ' + R[nx].name + '</h3><p>Move up whenever you like; it is your call, and one tap takes you back.</p>' + 407 diffHtml(cur, nx, st) + 408 '<div class="actions"><button class="btn btn-primary" data-oal="move" data-v="' + nx + '">Move up to ' + R[nx].name + '</button><button class="btn btn-quiet" data-oal="notyet">Not yet</button></div></div>'; 409 } else { 410 h += '<div class="next"><h3>This is the top step</h3><p>Maya runs within your limits. Step back to Semi or Manual at any time with one tap; it takes effect straight away.</p></div>'; 411 } 412 } else { 413 var up = !cur || idx(pv) > idx(cur); 414 h += '<p style="margin:0 0 4px;color:var(--oal-text-2)">' + esc(R[pv].head + ' ' + R[pv].lead) + '</p>' + diffHtml(cur || 'unset', pv, st) + 415 (pv !== 'manual' && up ? limitsHtml(st, pv) + topicsHtml(st, {}) : '') + 416 '<div class="actions"><button class="btn ' + (up ? 'btn-primary' : 'btn-warn') + '" data-oal="move" data-v="' + pv + '">' + (!cur ? 'Choose ' : up ? 'Move up to ' : 'Step back to ') + R[pv].name + '</button>' + 417 (cur ? '<button class="btn btn-quiet" data-oal="preview" data-v="' + cur + '">Stay on ' + R[cur].name + '</button>' : '') + '</div>'; 418 if (!cur) h += tuneHtml(st, ui); 419 } 420 h += '</div></div>'; 421 return h; 422 } 423 424 /* ââ THE LADDER AS ONE LINE (o9, 2026-09-29) âââââââââââââââââââââââââââ 425 Alex: "in the dashboard there's just a wall of text". Once a step is 426 chosen the ladder is one line: where they are, what that means today, and 427 Change, which opens the whole ladder above in place (previews, sensitive 428 topics, the tune panel, what went out). Not connected and paused fold into 429 the same line;
429 nothing repeats it. A coach who has not chosen a step sees 430 the ladder open, because that is the decision in front of them. */ 431 function lineHtml(st) { 432 var cur = st.level, name = R[cur].name, lead, what, extra = '', connect = ''; 433 if (st.paused) { 434 lead = 'Paused on <b>' + esc(name) + '</b>'; 435 what = 'nothing is going out until you resume'; 436 } else if (st.linkedin_connected === false) { 437 lead = 'On <b>' + esc(name) + '</b>'; 438 what = 'nothing goes out until LinkedIn is connected'; 439 connect = '<button type="button" class="btn btn-primary btn-sm" data-oal="connect-li">Connect</button>'; 440 } else { 441 lead = 'On <b>' + esc(name) + '</b>'; 442 var n = capOf(st, cur); 443 what = cur === 'manual' ? 'every comment waits for your Approve' 444 : cur === 'semi' ? perDay(n, true) + ' out without you' 445 : 'up to ' + perDay(n, true) + ' out without you'; 446 var gone = (st.went_out || []).filter(function (s) { return dayKey(s.at) === dayKey(Date.now()); }).length; 447 if (cur !== 'manual' && gone) extra = '<span class="ll-sep" aria-hidden="true">·</span><span class="ll-extra">' + gone + ' went out today</span>'; 448 } 449 return '<section class="ladder-line' + (st.paused ? ' paused' : st.linkedin_connected === false ? ' offline' : '') + '" aria-label="How much Maya does on her own" data-oal-line="' + esc(cur) + '">' + 450 '<span class="ll-text"><span class="ll-step">' + lead + '</span><span class="ll-sep" aria-hidden="true">·</span><span class="ll-what">' + esc(what) + '</span>' + extra + '</span>' + 451 '<span class="ll-acts">' + connect + '<button type="button" class="btn btn-quiet btn-sm" data-oal="open" aria-expanded="false">Change</button></span></section>'; 452 } 453 /* What the dashboard shows: the line, or the ladder open. */ 454 function panelView(st, ui) { 455 var chosen = st.level === 'manual' || st.level === 'semi' || st.level === 'full'; 456 return (chosen && !(ui && ui.open)) ? lineHtml(st) : panelHtml(st, ui || {}); 457 } 458 459 /* ââ 1. THE ONBOARDING STEP (prototype: startView) ââââââââââââââââââââ */ 460 function stepRows(st, ui) { 461 var lv = ui.level; 462 var semiCap = ui.cap, fullCap = capOf(st, 'full'); 463 var commentRow = lv === 'manual' 464 ? ['Maya drafts; each one waits for your Approve.', 'you', 'Waits for you'] 465 : lv === 'semi' 466 ? ['Up to ' + semiCap + ' a day in your style go out without you. Sensitive topics wait.', 'maya', 'Within ' + semiCap + ' a day'] 467 : ['Up to ' + fullCap + ' a day go out without you. Sensitive topics wait.', 'maya', 'Within ' + fullCap + ' a day']; 468 var noteOk = !!(ui.note && ui.note.edited); 469 var emailOk = !!(ui.email && ui.email.approved); 470 return [ 471 ['🔍', 'Find people who fit your community and read their profiles', 'Up to 80 a day, from the influencers and groups you picked. They may see that you viewed their profile.', 'maya', 'Runs by itself'], 472 ['♥', 'Like one recent post of each person', 'Up to 5 a day. The first warm hello.', 'maya', 'Runs by itself'], 473 ['💬', 'Comment on their posts', commentRow[0], commentRow[1], commentRow[2]], 474 ['➕', 'Send a connection request with your note', 'Up to 8 a day, about 10 days after the first like. You approve the note once; the requests then go out on their own.', noteOk ? 'maya' : 'you', noteOk ? 'Runs, with your note' : 'Needs your OK first'], 475 ['✉', 'Email a welcome to people who accept', 'Invites them into ' + (ui.community || 'your community') + ', from your address.', 'maya', emailOk ? 'Your approved version' : 'Our standard welcome for now'], 476 ['🗨', 'Send LinkedIn messages', 'Welcome or follow-up DMs.', 'off', 'Only if you allow them'] 477 ]; 478 } 479 480 function stepHtml(st, ui) { 481 var h = '<div class="phead"><div><div class="eyebrow">Outreach · Before LinkedIn</div><h1>Before anything goes out in your name</h1>' + 482 '<p class="sub">This is everything Outreach will do from your LinkedIn, and what will wait for you. Read it, change what you want, choose how much Maya does, then connect. It starts as
482soon as LinkedIn connects.</p></div>' + 483 '<div class="hstatus"><span class="status idle"><span class="dot"></span>Not connected yet</span></div></div>'; 484 h += '<div class="stepbar"><div class="done"><span class="n">✓</span>Community</div><div class="done"><span class="n">✓</span>Who to reach</div><div class="done"><span class="n">✓</span>First sources</div>' + 485 '<div class="now"><span class="n">4</span>How much Maya does</div><div><span class="n">5</span>Connect LinkedIn</div></div>'; 486 487 h += '<div class="grid2"><div>'; 488 /* What starts in your name */ 489 h += '<div class="card"><div class="card-head"><div class="card-title">What starts in your name</div><span class="card-meta">On ' + R[ui.level].name + '</span></div>'; 490 stepRows(st, ui).forEach(function (r, i) { 491 h += '<div class="row"><div class="ri">' + r[0] + '</div><div class="rb"><div class="rt">' + esc(r[1]) + '</div><div class="rd">' + esc(r[2]) + '</div>'; 492 if (i === 3) h += noteHtml(ui); 493 if (i === 4) h += emailHtml(ui); 494 h += '</div><span class="tagk ' + r[3] + '">' + esc(r[4]) + '</span></div>'; 495 }); 496 h += '</div>'; 497 /* Example comments: real drafts from their own discovery */ 498 h += examplesHtml(st, ui); 499 h += '<div class="card"><div class="card-head"><div class="card-title">Never, on any step</div></div><div class="card-body"><ul class="plain never">' + 500 '<li>Post anything on your own LinkedIn feed.</li><li>Message people you are already connected to.</li><li>Change your LinkedIn profile or settings.</li><li>Send anything while Outreach is paused.</li><li>Comment on a sensitive topic on her own (below).</li></ul>' + 501 topicsHtml(st, { manual: ui.level === 'manual' }) + '</div></div>'; 502 h += '</div><div>'; 503 504 /* How much Maya does */ 505 h += '<div class="card"><div class="card-head"><div class="card-title">How much do you want Maya to do?</div></div><div class="card-body" style="padding-top:6px">' + 506 optHtml('manual', ui, 'Manual: I approve every comment', 'Most people start here. Connection requests go out with the note you approved.') + 507 optHtml('semi', ui, 'Semi: up to ' + perDay(ui.cap, true) + ' out without me', 'In my style. I see each one after, with an undo until it posts. Sensitive topics wait.') + 508 (ui.level === 'semi' ? '<div class="opt-cap"><span>Comments a day without me</span><span class="stepper" style="margin:0"><button data-oal="scap" data-v="-1" aria-label="One fewer"' + (ui.cap <= 1 ? ' disabled' : '') + '>−</button><span class="v">' + ui.cap + '</span><button data-oal="scap" data-v="1" aria-label="One more"' + (ui.cap >= 8 ? ' disabled' : '') + '>+</button></span></div>' : '') + 509 optHtml('full', ui, 'Full: up to ' + perDay(capOf(st, 'full'), true) + ' out without me', 'I am only asked when something is unusual. Sensitive topics still wait.') + 510 '<div class="oal-note">You can move up, or step back, at any time from the top of your Outreach dashboard.</div></div></div>'; 511 512 /* Connect */ 513 var noteNeeded = ui.note && ui.note.body && !ui.note.edited; 514 h += '<div class="card"><div class="card-head"><div class="card-title">Then connect LinkedIn</div></div><div class="card-body">' + 515 '<p style="margin:0;color:var(--oal-text-2);font-size:13.5px">Maya begins within the hour; the first thing anyone might notice is a profile view.' + (ui.level === 'manual' ? ' Every comment waits for you in Comments.' : ' Comments within your limit go out in your name; the rest wait for you in Comments.') + '</p>' + 516 (noteNeeded ? '<div class="advice" style="margin:12px 0 0">Read your connection note first and press âThese are my wordsâ. It goes on every request.</div>' : '') + 517 '<div class="actions"><button class="btn btn-primary" data-oal="connect"' + (noteNeeded || ui.busy ? ' disabled' : '') + '>' + (ui.busy ? 'Savingâ¦' : 'Save and connect LinkedIn') + '</button>' + 518 '<span class="card-meta">30 seconds with the Chrome extension. We never see your password.</span></div></div></div>'; 519 h += '</div></div>'; 520 return h; 521 } 522 function optHtml(level, ui, title, desc) { 523 return '<button class="opt' + (ui.level === level ? ' on' : '') + '" data-oal="level" data-v="' + level + '"><span class="radio"></span><span><span class="ot">' + esc(title) + '</span><br><span class="od">' + esc(desc) + '</span></span></button>'; 524 } 525 function noteHtml(ui) { 526 var n = ui.note || {}; 527 if (!n.body) return n.loading ? '<div class="rd" style="margin-top:6px">Writing your noteâ¦</div>' : ''; 528 if (!ui.seeNote) return '<button class="linkish" data-oal="seeNote">Read and edit your note' + (n.edited ? ' (approved)' : '') + '</button>'; 529 return '<div class="see"><div class="rd" style="margin-bottom:6px">Written by Maya from your community description. Read it closely: it goes on every request, in your name, after âHi <first name>,â.</div>' + 530 '<textarea id="oal-note-ta" maxlength="' + (n.limit || 300) + '">' + esc(n.body) + '</textarea>' + 531 '<div class="meta"><span>The same for everyone apart from the greeting</span><span id="oal-note-count">' + String(n.body).length + ' / ' + (n.limit || 300) + '</span></div>' + 532 '<div class="actions" style="margin-top:8px"><button class="btn btn-primary btn-sm" data-oal="saveNote">' + (n.edited && !n.dirty ? 'Approved' : 'These are my words') + '</button><button class="btn btn-quiet btn-sm" data-oal="seeNote">Close</button></div></div>'; 533 } 534 function emailHtml(ui) { 535 var e = ui.email || {}; 536 if (!e.loaded) return ''; 537 if (!ui.seeEmail) return '<button class="linkish" data-oal="seeEmail">Read the welcome email</button>'; 538 var body = String(e.body || '').replace(/<\s*br\s*\/?>/gi, '\n').replace(/<\/p>/gi, '\n\n').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').trim(); 539 return '<div class="see"><div class="rt" style="font-size:13.5px">Subject: ' + esc(e.subject || '') + '</div>' + 540 '<div class="rd" style="margin-top:6px;color:var(--oal-text-2);white-space:pre-line">' + esc(body.slice(0, 900)) + '</div>' + 541 '<div class="rd" style="margin-top:6px">
541' + (e.approved ? 'This is your approved welcome.' : (e.own ? 'Not approved yet, so our standard welcome goes out until you approve it.' : 'This is our standard welcome. It goes out until you approve your own.')) + '</div>' + 542 '<div class="actions" style="margin-top:8px">' + (e.own && !e.approved ? '<button class="btn btn-primary btn-sm" data-oal="okEmail">Approve this email</button>' : '') + 543 '<a class="btn btn-quiet btn-sm" href="/app.html?module=community&page=community-emails&ce_tab=welcome" target="_blank" rel="noopener">Edit in Community, Emails</a><button class="btn btn-quiet btn-sm" data-oal="seeEmail">Close</button></div></div>'; 544 } 545 function examplesHtml(st, ui) { 546 var ex = ui.examples || {}; 547 var h = '<div class="card"><div class="card-head"><div class="card-title">How your comments will sound</div><span class="card-meta">Real drafts, from posts Maya found for you</span></div>'; 548 h += exampleItemsHtml(ex); 549 h += '<div class="card-body" style="border-top:1px solid var(--oal-border-soft)">' + 550 '<div class="sect-h">Pick a style by what you want comments to do</div>' + 551 (st.styles || []).map(function (s) { 552 return '<button class="style-opt' + (ui.style === s.id ? ' on' : '') + '" data-oal="pickStyle" data-v="' + esc(s.id) + '"><b>' + esc(s.name) + '</b><span>' + esc(s.purpose) + '</span></button>'; 553 }).join('') + 554 langLine({ voice: { spelling: (st.voice && st.voice.detected && st.voice.detected.spelling === ui.spelling) ? null : ui.spelling, 555 detected: st.voice && st.voice.detected }, styles: st.styles }, false) + 556 '<div class="sect-h" style="margin-top:16px">Tell Maya how to frame your comments</div>' + 557 '<textarea class="oal-ta" id="oal-direction" placeholder="For example: never mention my programme, and use first names.">' + esc(ui.direction || '') + '</textarea>' + 558 '<div class="actions" style="margin-top:8px"><button class="btn btn-sm" data-oal="retry"' + (ex.loading ? ' disabled' : '') + '>Write the examples again</button>' + 559 (ex.limit_reached ? '<span class="card-meta">That is todayâs examples; your choices still apply to every comment.</span>' : '<span class="card-meta">Your style, English and notes apply to every comment Maya drafts.</span>') + '</div>' + 560 '</div></div>'; 561 return h; 562 } 563 564 /* ââ Runtime: the dashboard panel âââââââââââââââââââââââââââââââââââââ */ 565 var P = { el: null, st: null, ui: { open: false, preview: null, join: null, tune: false, examples: null, direction: null } }; 566 567 function paintPanel() { 568 if (!P.el || !P.st) return; 569 var dta = document.getElementById('oal-p-direction'); 570 if (dta) P.ui.direction = dta.value; 571 P.el.classList.add('oal'); 572 P.el.innerHTML = panelView(P.st, P.ui); 573 } 574 function loadJoin() { 575 return call('/community-hosting/settings').then(function (r) { 576 var m = joinMode(r && r.settings); 577 P.ui.join = m; 578 return m; 579 }); 580 } 581 function mountPanel(el) { 582 if (!el) return Promise.resolve(); 583 /* The ladder sits after the trial starts: a preview account (no Outreach 584 yet, everything paused) is not offered it. */ 585 try { if (typeof isOutreachPreview === 'function' && isOutreachPreview()) { el.innerHTML = ''; return Promise.resolve(); } } catch (_) {} 586 P.el = el; 587 if (!el.__oalWired) { el.addEventListener('click', onPanelClick); el.__oalWired = true; } 588 return Promise.all([call('/outreach-customer/autonomy'), loadJoin()]).then(function (res) { 589 var st = res[0]; 590 if (!st || st.error) { el.innerHTML = ''; return; } 591 P.st = st; 592 P.ui.preview = null; 593 paintPanel(); 594 }); 595 } 596 /* Pause or resume anywhere (the amber bar, Settings): redraw from the server 597 so the panel never says "Paused" after a Resume (round 6, r2 F5). */ 598 if (typeof document !== 'undefined' && document.addEventListener) { 599 document.addEventListener('cb:outreach-run-changed', function () { 600 if (P.el && P.st && document.body.contains(P.el)) mountPanel(P.el); 601 }); 602 } 603 function choose(level, cap) { 604 var from = P.st && P.st.level; 605 return call('/outreach-customer/autonomy', { method: 'POST', body: { level: level, cap: cap } }).then(function (st) { 606 if (!st || !st.ok) { toast('That did not save. Nothing changed; try again.'); return; } 607 P.st = st; P.ui.preview = null; 608 /* The first choice is the decision made: the ladder folds to its line. */ 609 if (!from || from === 'unset') P.ui.open = false;
610 paintPanel(); 611 var up = !from || from === 'unset' || idx(level) > idx(from); 612 if (cap != null && from === level) toast('Saved. Up to ' + perDay(capOf(st, level), true) + ' out without you.'); 613 else toast(up ? 'You are on ' + R[level].name + ' now. Step back any time; it takes effect straight away.' 614 : 'Back on ' + R[level].name + '. ' + (level === 'manual' 615 ? (Number(st.brought_back) > 0 ? st.brought_back + (Number(st.brought_back) === 1 ? ' comment Maya queued is' : ' comments Maya queued are') + ' back in your list.' : 'Every comment waits for you again.') 616 : 'Comments drop back to ' + capOf(st, 'semi') + ' a day.')); 617 try { document.dispatchEvent(new CustomEvent('cb:outreach-autonomy-changed', { detail: { level: level } })); } catch (_) {} 618 }); 619 } 620 function focusIn(sel) { 621 try { var f = P.el && P.el.querySelector(sel); if (f && f.focus) f.focus({ preventScroll: true }); } catch (_) {} 622 } 623 function keepDirection() { 624 var ta = document.getElementById('oal-p-direction'); 625 if (ta) P.ui.direction = ta.value; 626 } 627 function loadPanelExamples(fresh) { 628 P.ui.examples = Object.assign({}, P.ui.examples || {}, { loading: true, error: false }); 629 keepDirection(); paintPanel(); 630 return call('/outreach-customer/autonomy/examples', { method: fresh ? 'POST' : 'GET', body: fresh ? {}
630 : undefined }).then(function (r) { 631 if (!r || r.error) P.ui.examples = { items: (P.ui.examples && P.ui.examples.items) || [], error: true }; 632 else P.ui.examples = { items: r.items || [], limit_reached: !!r.limit_reached }; 633 keepDirection(); paintPanel(); 634 }); 635 } 636 function onPanelClick(e) { 637 var b = e.target.closest('[data-oal]'); if (!b || !P.st) return; 638 var a = b.getAttribute('data-oal'), v = b.getAttribute('data-v'); 639 if (a === 'open') { P.ui.open = true; paintPanel(); focusIn('[data-oal="close"]'); return; } 640 if (a === 'close') { keepDirection(); P.ui.open = false; P.ui.preview = null; paintPanel(); focusIn('[data-oal="open"]'); return; } 641 if (a === 'connect-li') { 642 if (typeof window._ttOpenReconnect === 'function') window._ttOpenReconnect(); 643 else if (typeof window.startLinkedInConnect === 'function') window.startLinkedInConnect(); 644 return; 645 } 646 if (a === 'preview') { P.ui.preview = (v === P.st.level) ? null : v; paintPanel(); return; } 647 /* The cap set in the preview is the cap saved (round 6, r2 F1: pressing â 648 to 2 then "Choose Semi" saved 3). */ 649 if (a === 'move') { choose(v, v === 'semi' ? capOf(P.st, 'semi') : v === 'full' ? capOf(P.st, 'full') : undefined); return; } 650 if (a === 'topic') { saveTopic(P.st, v, paintPanel); return; } 651 if (a === 'tune') { 652 keepDirection(); 653 P.ui.tune = !P.ui.tune; paintPanel(); 654 if (P.ui.tune && !P.ui.examples) loadPanelExamples(false); 655 return; 656 } 657 if (a === 'retryEx') { 658 keepDirection(); 659 call('/outreach-customer/autonomy/voice', { method: 'POST', body: { direction: P.ui.direction || '' } }).then(function (r) { 660 if (!r || !r.ok) { toast('That did not save. Try again.'); return; } 661 P.st.voice = P.st.voice || {}; P.st.voice.direction = P.ui.direction || ''; 662 loadPanelExamples(true); 663 }); 664 return; 665 } 666 if (a === 'notyet') { toast('Fine. Nothing changes.'); return; } 667 if (a === 'fcap') { 668 var nf = Math.max(1, Math.min(8, capOf(P.st, 'full') + Number(v))); 669 if (P.st.level === 'full') choose('full', nf); else { P.st.full_cap = nf; paintPanel(); } 670 return; 671 } 672 if (a === 'cap') { 673 var n = Math.max(1, Math.min(8, capOf(P.st, 'semi') + Number(v))); 674 if (P.st.level === 'semi') choose('semi', n); else { P.st.semi_cap = n; paintPanel(); } 675 return; 676 } 677 if (a === 'style' || a === 'spelling') { 678 var body = a === 'style' ? { style: v } : { spelling: v }; 679 call('/outreach-customer/autonomy/voice', { method: 'POST', body: body }).then(function (r) { 680 if (!r || !r.ok) { toast('That did not save. Try again.'); return; } 681 P.st.voice = P.st.voice || {}; 682 if (a === 'style') { P.st.voice.style = v; P.st.voice.style_saved = true; } else P.st.voice.spelling = v; 683 keepDirection(); paintPanel(); 684 if (P.ui.tune) loadPanelExamples(true); 685 toast(a === 'style' ? 'Saved. Maya drafts every new comment as ' + (styleName(P.st, v) || v).toLowerCase() + '.' : 'Saved. Maya writes in ' + (v === 'uk' ? 'UK' : 'US') + ' English from now on.'); 686 }); 687 return; 688 } 689 if (a === 'undo') { 690 b.disabled = true; 691 call('/outreach-customer/autonomy/undo/' + encodeURIComponent(v), { method: 'POST', body: {} }).then(function (r) { 692 if (!r || !r.ok) { b.disabled = false; toast(r && r.error === 'already_posted' ? 'It has already posted. You can delete it on LinkedIn.' : 'That did not work. Try again.'); return; } 693 toast('Undone. It will not post. It waits for you in Comments now.'); 694 mountPanel(P.el); 695 }); 696 return; 697 } 698 if (a === 'join') { 699 if (typeof window.showCommunitySettings === 'function') window.showCommunitySettings('controls'); 700 else window.location.href = '/app.html?page=community-settings'; 701 } 702 } 703 704 /* ââ Runtime: the onboarding step âââââââââââââââââââââââââââââââââââââ */ 705 var S = { el: null, st: null, ui: null }; 706 707 function paintStep() { 708 if (!S.el || !S.st) return; 709 var ta = document.getElementById('oal-direction'); 710 if (ta) S.ui.direction = ta.value; 711 var nt = document.getElementById('oal-note-ta'); 712 if (nt && S.ui.note) { if (nt.value !== S.ui.note.body) { S.ui.note.body = nt.value; S.ui.note.dirty = true; } } 713 S.el.innerHTML = '<div class="oal oal-page"><div class="oal-page-inner">' + stepHtml(S.st, S.ui) + '</div></div>'; 714 } 715 function loadExamples(fresh) { 716 S.ui.examples = Object.assign({}, S.ui.examples || {}, { loading: true, error: false }); 717 paintStep(); 718 return call('/outreach-customer/autonomy/examples', { method: fresh ? 'POST' : 'GET', body: fresh ? {}
718 : undefined }).then(function (r) { 719 if (!r || r.error) { S.ui.examples = { items: (S.ui.examples && S.ui.examples.items) || [], error: true }; } 720 else S.ui.examples = { items: r.items || [], limit_reached: !!r.limit_reached }; 721 paintStep(); 722 }); 723 } 724 function loadNote() { 725 S.ui.note = { loading: true }; 726 return call('/outreach-customer/settings/connection-message').then(function (r) { 727 if (r && r.body) { S.ui.note = { body: r.body, edited: !!r.edited, limit: r.limit || 300 }; paintStep(); return; } 728 if (r && r.has_description) { 729 return call('/outreach-customer/settings/connection-message/generate', { method: 'POST', body: {} }).then(function (g) { 730 S.ui.note = (g && g.body) ? { body: g.body, edited: false, limit: r.limit || 300 } : { body: '' }; 731 paintStep(); 732 }); 733 } 734 S.ui.note = { body: '' }; paintStep(); 735 }); 736 } 737 function loadEmail() { 738 return call('/outreach-customer/settings/welcome-email').then(function (r) { 739 if (!r || !r.has_community) { S.ui.email = { loaded: false }; return; } 740 var own = r.welcome_email && r.welcome_email.subject && r.welcome_email.body_html; 741 var d = r.default_welcome_email || {}; 742 S.ui.email = own 743 ? { loaded: true, own: true, approved: !!(r.welcome_email.approved_at || r.welcome_email.approved), subject: r.welcome_email.subject, body: r.welcome_email.body_html } 744 : { loaded: true, own: false, approved: false, subject: d.subject || 'Welcome', body: d.body_html || d.body || '' }; 745 paintStep(); 746 }); 747 } 748 function showStep(el) { 749 S.el = el || document.getElementById('page-outreach-ladder'); 750 if (!S.el) return Promise.resolve(); 751 if (!S.el.__oalWired) { S.el.addEventListener('click', onStepClick); S.el.addEventListener('input', onStepInput); S.el.__oalWired = true; } 752 S.el.innerHTML = '<div class="oal oal-page"><div class="oal-page-inner"><div class="eyebrow">Outreach</div><h1>Loadingâ¦</h1></div></div>'; 753 return call('/outreach-customer/autonomy').then(function (st) { 754 if (!st || st.error) { S.el.innerHTML = '<div class="oal oal-page"><div class="oal-page-inner"><h1>Something went wrong</h1><p class="sub">Refresh the page to try again.</p></div></div>'; return; } 755 S.st = st; 756 var v = st.voice || {}; 757 S.ui = { 758 level: st.chosen ? st.level : 'manual', 759 cap: st.semi_cap || 3, 760 style: v.style && v.style !== 'custom' ? v.style : 'warm', 761 spelling: v.spelling || (v.detected && v.detected.spelling) || 'us', 762 direction: v.direction || '', 763 community: null, note: { loading: true }, email: { loaded: false }, examples: { loading: true }, 764 seeNote: false, seeEmail: false, busy: false 765 }; 766 try { if (typeof currentUser !== 'undefined' && currentUser && currentUser.community_name) S.ui.community = currentUser.community_name; } catch (_) {} 767 paintStep(); 768 loadNote(); loadEmail(); 769 /* The first examples are written in the style shown, so save it first 770 when the coach has none yet (it is the choice on screen). */ 771 var styleUnsaved = !v.style || v.style_saved === false; 772 var first = (styleUnsaved || !v.spelling) 773 ? call('/outreach-customer/autonomy/voice', { method: 'POST', body: { style: styleUnsaved ? S.ui.style : null, spelling: v.spelling ? null : S.ui.spelling } }) 774 : Promise.resolve(); 775 first.then(function () { loadExamples(false); }); 776 }); 777 } 778 function onStepInput(e) { 779 if (e.target && e.target.id === 'oal-note-ta' && S.ui.note) { 780 S.ui.note.body = e.target.value; S.ui.note.dirty = true; 781 var c = document.getElementById('oal-note-count'); if (c) c.textContent = e.target.value.length + ' / ' + (S.ui.note.limit || 300); 782 } 783 } 784 function onStepClick(e) { 785 var b = e.target.closest('[data-oal]'); if (!b || !S.st) return; 786 var a = b.getAttribute('data-oal'), v = b.getAttribute('data-v'); 787 if (a === 'level') { S.ui.level = v; paintStep(); return; } 788 if (a === 'topic') { saveTopic(S.st, v, paintStep); return; } 789 if (a === 'scap') { S.ui.cap = Math.max(1, Math.min(8, S.ui.cap + Number(v))); paintStep(); return; } 790 if (a === 'seeNote') { S.ui.seeNote = !S.ui.seeNote; paintStep(); return; } 791 if (a === 'seeEmail') { S.ui.seeEmail = !S.ui.seeEmail; paintStep(); return; } 792 if (a === 'saveNote') { 793 var nt = document.getElementById('oal-note-ta'); 794 var text = nt ? nt.value.trim() : (S.ui.note.body || ''); 795 if (!text) { toast('Your note cannot be empty.'); return; } 796 call('/outreach-customer/settings/connection-message', { method: 'PUT', body: { text: text } }).then(function (r) { 797 if (!r || r.ok === false || r.error) { 798 toast(r && r.reason === 'too_long' ? 'Your note is ' + r.length + ' characters; the most is ' + r.limit + '.' : 'That did not save. Try again.'); 799 return; 800 } 801 S.ui.note = { body: (r.text || text), edited: true, limit: S.ui.note.limit }; 802 S.ui.seeNote = false; paintStep(); 803 toast('Saved. Your words are never rewritten.'); 804 }); 805 return; 806 } 807 if (a === 'okEmail') { 808 call('/outreach-customer/settings/welcome-email/approve', { method: 'POST', body: {} }).then(function (r) {
809 if (!r || !r.welcome_email) { toast('That did not save. Try again.'); return; } 810 S.ui.email.approved = true; paintStep(); 811 toast('Approved. This is the welcome email Maya sends.'); 812 }); 813 return; 814 } 815 if (a === 'pickStyle') { 816 S.ui.style = v; paintStep(); 817 call('/outreach-customer/autonomy/voice', { method: 'POST', body: { style: v } }).then(function () { loadExamples(true); }); 818 return; 819 } 820 if (a === 'spelling') { 821 S.ui.spelling = v; paintStep(); 822 call('/outreach-customer/autonomy/voice', { method: 'POST', body: { spelling: v } }).then(function () { loadExamples(true); }); 823 return; 824 } 825 if (a === 'retry') { 826 var ta = document.getElementById('oal-direction'); 827 S.ui.direction = ta ? ta.value : S.ui.direction; 828 call('/outreach-customer/autonomy/voice', { method: 'POST', body: { direction: S.ui.direction || '' } }).then(function () { loadExamples(true); }); 829 return; 830 } 831 if (a === 'connect') { 832 var dta = document.getElementById('oal-direction'); 833 if (dta) S.ui.direction = dta.value; 834 S.ui.busy = true; paintStep(); 835 call('/outreach-customer/autonomy/voice', { method: 'POST', body: { style: S.ui.style, spelling: S.ui.spelling, direction: S.ui.direction || '' } }) 836 .then(function () { return call('/outreach-customer/autonomy', { method: 'POST', body: { level: S.ui.level, cap: S.ui.level === 'semi' ? S.ui.cap : undefined } }); }) 837 .then(function (st) { 838 S.ui.busy = false; 839 if (!st || !st.ok) { paintStep(); toast('That did not save. Nothing changed; try again.'); return; } 840 try { if (typeof currentUser !== 'undefined' && currentUser) currentUser.outreach_needs_autonomy = false; } catch (_) {} 841 var go = function () { if (typeof window.startLinkedInConnect === 'function') window.startLinkedInConnect(); }; 842 if (typeof window.refreshOutreachOnboardingFlags === 'function') Promise.resolve(window.refreshOutreachOnboardingFlags()).then(go, go); 843 else go(); 844 }); 845 } 846 } 847 848 function showOutreachLadder() { 849 try { 850 if (typeof currentModule !== 'undefined') currentModule = 'outreach'; 851 if (typeof currentPage !== 'undefined') currentPage = 'outreach-ladder'; 852 } catch (_) {} 853 if (typeof showPage === 'function') showPage('outreach-ladder'); 854 if (typeof renderSidebar === 'function') { try { renderSidebar(); } catch (_) {} } 855 return showStep(document.getElementById('page-outreach-ladder')); 856 } 857 858 var pure = { RUNGS: RUNGS, R: R, matrixRows: matrixRows, diffFor: diffFor, diffHtml: diffHtml, panelHtml: panelHtml, lineHtml: lineHtml, panelView: panelView, stepHtml: stepHtml, joinMode: joinMode, JOIN: JOIN, 859 topicsHtml: topicsHtml, tuneHtml: tuneHtml, exampleItemsHtml: exampleItemsHtml, heldLine: heldLine, expiryLine: expiryLine, planLineHtml: planLineHtml, 860 styleNote: styleNote, limitsHtml: limitsHtml }; 861 if (typeof window !== 'undefined') { 862 window.App = window.App || {}; 863 window.App.OutreachAutonomy = { mountPanel: mountPanel, showStep: showStep, expiryLine: expiryLine, planLineHtml: planLineHtml, _pure: pure }; 864 window.showOutreachLadder = showOutreachLadder; 865 } 866 if (typeof module !== 'undefined' && module.exports) module.exports = pure; 867})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.