PageSourceSearch

https://app.replicants.ai/js/outreach-autonomy.js?v=20260929o9b

js replicants.ai collected 2026-10-05 12:30:02 UTC 61,841 bytes, 867 lines download raw bytes

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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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' : '') + '>&minus;</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' : '') + '>&minus;</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      ['&#128269;', '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      ['&#9829;', 'Like one recent post of each person', 'Up to 5 a day. The first warm hello.', 'maya', 'Runs by itself'],
473      ['&#128172;', 'Comment on their posts', commentRow[0], commentRow[1], commentRow[2]],
474      ['&#10133;', '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      ['&#9993;', '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      ['&#128488;', '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 &middot; 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">&#10003;</span>Community</div><div class="done"><span class="n">&#10003;</span>Who to reach</div><div class="done"><span class="n">&#10003;</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' : '') + '>&minus;</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 &lt;first name&gt;,”.</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(/&nbsp;/g, ' ').replace(/&amp;/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&amp;page=community-emails&amp;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.