PageSourceSearch

https://corefitness-tracker.pages.dev/js/coaching-context.js

js corefitness-tracker.pages.dev collected 2026-10-03 06:39:43 UTC 22,492 bytes, 490 lines download raw bytes

1// =====================================================
2// COACHING CONTEXT -- Persistent AI Coaching Context
3// =====================================================
4// Table: client_ai_context
5//   client_id   uuid PRIMARY KEY REFERENCES profiles(id)
6//   constraints text[]
7//   motivators  text[]
8//   coach_notes text
9//   updated_at  timestamptz
10
11var _ccxCache     = {};    // clientId -> { constraints:[], motivators:[], coach_notes:'' } | null
12var _ccxAllLoaded = false;
13var _ccxEditState = null;  // active editor working copy
14
15// -- LOAD / CACHE -----------------------------------------
16
17async function ccxPreloadAll() {
18  if (_ccxAllLoaded) return;
19  try {
20    var res  = await sb.from('client_ai_context').select('*');
21    var rows = res.data || [];
22    rows.forEach(function(r) {
23      _ccxCache[r.client_id] = {
24        constraints: r.constraints || [],
25        motivators:  r.motivators  || [],
26        coach_notes: r.coach_notes || ''
27      };
28    });
29    _ccxAllLoaded = true;
30  } catch(e) { /* non-fatal -- coach features degrade gracefully */ }
31}
32
33async function ccxGet(clientId) {
34  if (_ccxCache.hasOwnProperty(clientId)) return _ccxCache[clientId];
35  try {
36    var res = await sb.from('client_ai_context')
37      .select('*').eq('client_id', clientId).maybeSingle();
38    _ccxCache[clientId] = res.data ? {
39      constraints: res.data.constraints || [],
40      motivators:  res.data.motivators  || [],
41      coach_notes: res.data.coach_notes || ''
42    } : null;
43  } catch(e) { _ccxCache[clientId] = null; }
44  return _ccxCache[clientId];
45}
46
47function ccxGetCached(clientId) {
48  return _ccxCache.hasOwnProperty(clientId) ? _ccxCache[clientId] : null;
49}
50
51// -- AI PROMPT BLOCK --------------------------------------
52// Returns a formatted string ready for injection into any AI prompt.
53
54function ccxBuildPromptBlock(context) {
55  if (!context) return '';
56  var parts = [];
57  if (context.constraints && context.constraints.length > 0) {
58    parts.push('Constraints: ' + context.constraints.join('. ') + '.');
59  }
60  if (context.motivators && context.motivators.length > 0) {
61    parts.push('Motivators: ' + context.motivators.join('. ') + '.');
62  }
63  if (context.coach_notes && context.coach_notes.trim()) {
64    parts.push('Coach notes: ' + context.coach_notes.trim());
65  }
66  if (parts.length === 0) return '';
67  return 'Client context:\n' + parts.join('\n');
68}
69
70// -- CONTEXT BADGE ----------------------------------------
71// Small clickable chip shown on Coach Today cards and PT dashboard rows.
72
73function ccxBadgeHtml(clientId, clientName) {
74  var context = ccxGetCached(clientId);
75  var hasCtx  = context && (
76    (context.constraints && context.constraints.length > 0) ||
77    (context.motivators  && context.motivators.length  > 0) ||
78    (context.coach_notes && context.coach_notes.trim().length > 0)
79  );
80  var safeId   = clientId.replace(/['"\\]/g, '');
81  var safeName = (clientName || '').replace(/['"\\]/g, '').replace(/[\r\n]/g, ' ');
82  if (hasCtx) {
83    return '<span onclick="openCoachingContextEditor(\'' + safeId + '\',\'' + safeName + '\',null,null)" ' +
84      'style="display:inline-flex;align-items:center;background:#0f2a1a;border:1px solid #3ecf8e44;' +
85      'color:#3ecf8e;border-radius:10px;padding:2px 8px;font-size:9px;cursor:pointer;white-space:nowrap;user-select:none;">' +
86      'AI Context</span>';
87  }
88  return '<span onclick="openCoachingContextEditor(\'' + safeId + '\',\'' + safeName + '\',null,null)" ' +
89    'style="display:inline-flex;align-items:center;background:#1a1a1a;border:1px solid #2a2a2a;' +
90    'color:#444;border-radius:10px;padding:2px 8px;font-size:9px;cursor:pointer;white-space:nowrap;user-select:none;">' +
91    'Add Context</span>';
92}
93
94// -- PATTERN-BASED SUGGESTION BANNERS ---------------------
95// Shown on Coach Today cards when a pattern fires and the relevant
96// context section is empty. Tapping opens the editor pre-filled.
97
98var _CCX_SUGGESTIONS = {
99  protein_gap: {
100    section: 'constraints',
101    prefill: 'Protein is difficult to hit because ',
102    message: 'Protein gap detected. Is there a practical reason protein is hard to hit? Adding context improves future recommendations.'
103  },
104  weekend_collapse: {
105    section: 'constraints',
106    prefill: 'Social events or commitments at weekends, for example ',
107    message: 'Weekend logging drops consistently. Do social events, alcohol or family commitments play a role? Context c
107an help personalise coaching.'
108  },
109  weekend_calorie_spike: {
110    section: 'constraints',
111    prefill: 'Calorie spikes at weekends are likely because ',
112    message: 'Weekend calorie spikes are recurring. Do social events, dining out or alcohol feature regularly? Adding context helps.'
113  },
114  logging_fatigue: {
115    section: 'coach_notes',
116    prefill: 'Tracking becomes difficult when ',
117    message: 'Logging fatigue detected. Is there a known reason tracking becomes harder for this client?'
118  },
119  recovery_risk: {
120    section: 'constraints',
121    prefill: 'Calorie intake tends to be low because ',
122    message: 'Calories are consistently below target. Is there a practical or lifestyle reason? Context can help the AI give better advice.'
123  },
124  all_or_nothing: {
125    section: 'coach_notes',
126    prefill: 'Tends to disengage completely during certain periods -- this may be linked to ',
127    message: 'All-or-nothing pattern detected. Is there a known trigger for disengagement? Context can personalise future coaching.'
128  }
129};
130
131function ccxGetPatternSuggestions(clientId) {
132  var context  = ccxGetCached(clientId);
133  var patterns = (typeof getBehaviourPatterns === 'function') ? getBehaviourPatterns(clientId) : [];
134
135  var hasConstraints = context && context.constraints && context.constraints.length > 0;
136  var hasMotivators  = context && context.motivators  && context.motivators.length  > 0;
137  var hasNotes       = context && context.coach_notes && context.coach_notes.trim().length > 0;
138
139  var suggestions = [];
140
141  // Pattern-based suggestions
142  if (patterns && patterns.length > 0) {
143    patterns.forEach(function(p) {
144      var def = _CCX_SUGGESTIONS[p.type];
145      if (!def) return;
146      if (def.section === 'constraints' && hasConstraints) return;
147      if (def.section === 'coach_notes' && hasNotes) return;
148      for (var i = 0; i < suggestions.length; i++) {
149        if (suggestions[i].section === def.section) return;
150      }
151      suggestions.push({
152        section:     def.section,
153        prefill:     def.prefill,
154        message:     def.message,
155        patternType: p.type
156      });
157    });
158  }
159
160  // Motivator suggestion: if no motivators exist and low engagement or reengage pattern fires
161  if (!hasMotivators) {
162    var lowEngagement = patterns && patterns.some(function(p) {
163      return p.type === 'low_engagement' || p.type === 'logging_fatigue' || p.type === 'all_or_nothing';
164    });
165    // Also suggest if no motivators at all regardless of pattern, but only if no context exists at all
166    var noContextAtAll = !hasConstraints && !hasNotes;
167    if (lowEngagement || noContextAtAll) {
168      var alreadyHasMotivatorSuggestion = suggestions.some(function(s) { return s.section === 'motivators'; });
169      if (!alreadyHasMotivatorSuggestion) {
170        suggestions.push({
171          section:     'motivators',
172          prefill:     'Working towards ',
173          message:     'No motivators added. What is keeping this client going? Adding a goal or event helps the AI make messages feel relevant and timely.',
174          patternType: null
175        });
176      }
177    }
178  }
179
180  return suggestions;
181}
182
183function ccxSuggestionBannerHtml(clientId, clientName) {
184  var suggestions = ccxGetPatternSuggestions(clientId);
185  if (suggestions.length === 0) return '';
186  var s        = suggestions[0];
187  var safeId   = clientId.replace(/['"\\]/g, '');
188  var safeName = (clientName || '').replace(/['"\\]/g, '').replace(/[\r\n]/g, ' ');
189  var safeSection = s.section.replace(/['"\\]/g, '');
190  // Encode prefill safely for inline onclick
191  var safePrefill = s.prefill.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
192  return '<div style="background:#0d1a0d;border:1px solid #1e3a1e;border-radius:8px;padding:9px 12px;margin-bottom:10px;">' +
193    '<div style="font-size:9px;color:#3ecf8e;letter-spacing:1.5px;text-transform:uppercase;font-family:\'DM Mono\',monospace;margin-bottom:4px;opacity:0.7;">Context Suggestion</div>' +
194    '<div style="font-size:11px;color:#5adf9e;line-height:1.5;margin-bottom:7px;">' + escapeHtml(s.message) + '</div>' +
195    '<button onclick="openCoachingContextEditor(\'' + safeId + '\',\'' + safeName + '\',\'' + safeSection + '\',\'' + safePrefill + '\')" ' +
196      'style="background:#1a3a1a;border:1px solid #3ecf8e55;color:#3ecf8e;border-radius:6px;padding:5px 12px;font-size:11px;cursor:pointer;font-family:inherit;">' +
197      'Add Context' +
198    '</button>' +
199  '</div>';
200}
201
202// -- EDITOR OVERLAY ----------------------------------------
203
204function openCoachingContextEditor(clientId, clientName, prefillSection, prefillText) {
205  var existing = document.getElementById('ccxEditorOverlay');
206  if (existing) existing.remove();
207
208  var context = ccxGetCached(clientId) || { constraints: [], motivators: [], coach_notes: '' };
209  _ccxEditState = {
210    clientId:    clientId,
211    clientName:  clientName || '',
212    constraints: context.constraints.slice(),
213    motivators:  context.motivators.slice(),
214    coach_notes: context.coach_notes || ''
215  };
216
217  var ov = document.createElement('div');
218  ov.id = 'ccxEditorOverlay';
219  ov.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:#0a0a0a;z-index:10000;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;';
220  ov.innerHTML = _ccxEditorHtml(clientName, prefillSection, prefillText);
221  document.body.appendChild(ov);
222  lockBodyScroll();
223
224  if (prefillSection && prefillSection !== 'coach_notes') {
225    var inputEl = document.getElementById('ccxInput_' + prefillSection);
226    if (inputEl && prefillText) { inputEl.value = prefillText; inputEl.focus(); }
227  }
228  if (prefillSection === 'coach_notes') {
229    var notesEl = document.getElementById('ccxNotesArea');
230    if (notesEl && prefillText) {
231      notesEl.value = prefillText;
232      notesEl.focus();
233      notesEl.setSelectionRange(notesEl.value.length, notesEl.value.length);
234    }
235  }
236}
237
238function closeCoachingContextEditor() {
239  var ov = document.getElementById('ccxEditorOverlay');
240  if (ov) ov.remove();
241  _ccxEditState = null;
242  unlockBodyScroll();
243}
244
245function _ccxEditorHtml(clientName, prefillSection, prefillText) {
246  var s         = _ccxEditState;
247  var firstName = (clientName || '').split(' ')[0] || clientName;
248
249  var notesPrefill = '';
250  if (prefillSection === 'coach_notes' && prefillText) {
251    notesPrefill = escapeHtml(prefillText);
252  } else {
253    notesPrefill = escapeHtml(s.coach_notes || '');
254  }
255
256  return '' +
257    '<div style="max-width:520px;margin:0 auto;padding:20px;">' +
258
259      '<div style="display:flex;align-items:center;gap:12px;margin-bottom:22px;padding-bottom:14px;border-bottom:1px solid #2a2a2a;">
259' +
260        '<button onclick="closeCoachingContextEditor()" style="background:#202020;border:1px solid #2a2a2a;color:#f5f5f5;border-radius:8px;padding:8px 14px;cursor:pointer;font-size:13px;">Back</button>' +
261        '<div>' +
262          '<div style="font-family:\'Bebas Neue\',sans-serif;font-size:22px;letter-spacing:1px;color:#f5f5f5;">AI Coaching Context</div>' +
263          '<div style="font-size:11px;color:#555;margin-top:2px;">' + escapeHtml(firstName) + ' - long-term coaching intelligence</div>' +
264        '</div>' +
265      '</div>' +
266
267      '<div style="font-size:11px;color:#555;line-height:1.6;margin-bottom:20px;padding:10px 14px;background:#111;border:1px solid #1e1e1e;border-radius:8px;">' +
268        'Used by the AI in every coaching feature. Not a weekly note -- keep it accurate and update it when circumstances change.' +
269      '</div>' +
270
271      _ccxSectionHtml('constraints', 'Constraints', '#e31c1c',
272        "Things that limit this client's options: work schedule, family meals, injuries, diet, budget.",
273        s.constraints,
274        (prefillSection === 'constraints' && prefillText) ? prefillText : '',
275        'e.g. Work canteen controls lunch') +
276
277      _ccxSectionHtml('motivators', 'Motivators', '#f0a84e',
278        'What drives this client: goals, upcoming events, personal reasons for training.',
279        s.motivators,
280        (prefillSection === 'motivators' && prefillText) ? prefillText : '',
281        'e.g. Holiday in August') +
282
283      '<div style="margin-bottom:22px;">' +
284        '<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:#4ec9f0;font-family:\'DM Mono\',monospace;margin-bottom:8px;">Coach Notes</div>' +
285        '<div style="font-size:11px;color:#555;margin-bottom:10px;">Free-text observations: communication style, behavioural tendencies, what works or what to avoid.</div>' +
286        '<textarea id="ccxNotesArea" rows="4" ' +
287          'style="width:100%;background:#111;border:1px solid #2a2a2a;border-radius:8px;padding:10px 12px;color:#f5f5f5;font-family:inherit;font-size:13px;resize:vertical;box-sizing:border-box;line-height:1.5;">' +
288          notesPrefill +
289        '</textarea>' +
290      '</div>' +
291
292      '<div style="display:flex;gap:8px;margin-bottom:12px;">' +
293        '<button onclick="ccxAISuggest()" id="ccxAISuggestBtn" ' +
294          'style="flex:1;background:#161616;border:1px solid #2a2a2a;color:#555;border-radius:8px;padding:11px;font-family:inherit;font-size:13px;cursor:pointer;">' +
295          'AI Suggestions' +
296        '</button>' +
297        '<button onclick="ccxSave()" id="ccxSaveBtn" ' +
298          'style="flex:2;background:#e31c1c;color:#fff;border:none;border-radius:8px;padding:11px;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;">' +
299          'Save Context' +
300        '</button>' +
301      '</div>' +
302
303      '<div id="ccxSaveStatus" style="text-align:center;font-size:12px;color:#555;min-height:18px;"></div>' +
304      '<div id="ccxAISuggestPanel" style="margin-top:10px;"></div>' +
305
306    '</div>';
307}
308
309function _ccxSectionHtml(section, label, accent, desc, tags, inputPrefill, placeholder) {
310  return '' +
311    '<div style="margin-bottom:22px;">' +
312      '<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:' + accent + ';font-family:\'DM Mono\',monospace;margin-bottom:8px;">' + label + '</div>' +
313      '<div style="font-size:11px;color:#555;margin-bottom:10px;">' + desc + '</div>' +
314      '<div id="ccxTags_' + section + '" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;">' + _ccxTagsHtml(section, tags) + '</div>' +
315      '<div style="display:flex;gap:8px;">' +
316        '<input id="ccxInput_' + section + '" type="text" placeholder="' + escapeHtml(placeholder) + '" ' +
317          'value="' + escapeHtml(inputPrefill || '') + '" ' +
318          'style="flex:1;background:#111;border:1px solid #2a2a2a;border-radius:8px;padding:9px 12px;color:#f5f5f5;font-family:inherit;font-size:13px;" ' +
319          'onkeydown="if(event.key===\'Enter\'){event.preventDefault();ccxAddTag(\'' + section + '\');}" />' +
320        '<button onclick="ccxAddTag(\'' + section + '\')" ' +
321          'style="background:#202020;border:1px solid #2a2a2a;color:#bbb;border-radius:8px;padding:9px 14px;font-family:inherit;font-size:13px;
321cursor:pointer;white-space:nowrap;">Add</button>' +
322      '</div>' +
323    '</div>';
324}
325
326function _ccxTagsHtml(section, tags) {
327  if (!tags || tags.length === 0) {
328    return '<div style="font-size:11px;color:#333;padding:2px 0;">None added yet.</div>';
329  }
330  return tags.map(function(tag, i) {
331    return '' +
332      '<span style="display:inline-flex;align-items:center;gap:6px;background:#1a1a1a;border:1px solid #2a2a2a;' +
333      'border-radius:16px;padding:5px 10px 5px 12px;font-size:12px;color:#bbb;">' +
334        escapeHtml(tag) +
335        '<button onclick="ccxRemoveTag(\'' + section + '\',' + i + ')" ' +
336          'style="background:#2a2a2a;border:none;color:#666;width:16px;height:16px;border-radius:50%;cursor:pointer;' +
337          'font-size:9px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;">' +
338          'x' +
339        '</button>' +
340      '</span>';
341  }).join('');
342}
343
344function ccxAddTag(section) {
345  if (!_ccxEditState) return;
346  var input = document.getElementById('ccxInput_' + section);
347  if (!input) return;
348  var val = input.value.trim();
349  if (!val) return;
350  _ccxEditState[section].push(val);
351  input.value = '';
352  var container = document.getElementById('ccxTags_' + section);
353  if (container) container.innerHTML = _ccxTagsHtml(section, _ccxEditState[section]);
354  input.focus();
355}
356
357function ccxRemoveTag(section, index) {
358  if (!_ccxEditState) return;
359  _ccxEditState[section].splice(index, 1);
360  var container = document.getElementById('ccxTags_' + section);
361  if (container) container.innerHTML = _ccxTagsHtml(section, _ccxEditState[section]);
362}
363
364async function ccxSave() {
365  if (!_ccxEditState) return;
366  var btn    = document.getElementById('ccxSaveBtn');
367  var status = document.getElementById('ccxSaveStatus');
368  if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
369
370  var notesEl = document.getElementById('ccxNotesArea');
371  var notes   = notesEl ? notesEl.value.trim() : _ccxEditState.coach_notes;
372
373  var payload = {
374    client_id:   _ccxEditState.clientId,
375    constraints: _ccxEditState.constraints.slice(),
376    motivators:  _ccxEditState.motivators.slice(),
377    coach_notes: notes,
378    updated_at:  new Date().toISOString()
379  };
380
381  try {
382    var res = await sb.from('client_ai_context').upsert(payload, { onConflict: 'client_id' });
383    if (res.error) throw new Error(res.error.message);
384
385    _ccxCache[_ccxEditState.clientId] = {
386      constraints: payload.constraints,
387      motivators:  payload.motivators,
388      coach_notes: payload.coach_notes
389    };
390
391    if (btn) {
392      btn.textContent   = 'Saved';
393      btn.disabled      = false;
394      btn.style.background = '#0a1a0a';
395      btn.style.border  = '1px solid #3ecf8e';
396      btn.style.color   = '#3ecf8e';
397    }
398    if (status) status.textContent = 'Saved.';
399    setTimeout(closeCoachingContextEditor, 700);
400  } catch(e) {
401    if (status) status.textContent = 'Could not save: ' + escapeHtml(e.message);
402    if (btn) { btn.textContent = 'Save Context'; btn.disabled = false; }
403  }
404}
405
406// -- AI SUGGESTIONS ----------------------------------------
407// Calls the /suggest-context worker route and renders suggestions
408// the coach can tap to pre-fill the editor (they still need to save).
409
410async function ccxAISuggest() {
411  if (!_ccxEditState) return;
412  var btn   = document.getElementById('ccxAISuggestBtn');
413  var panel = document.getElementById('ccxAISuggestPanel');
414  if (!btn || !panel) return;
415
416  btn.textContent = 'Thinking...';
417  btn.disabled    = true;
418
419  var patterns = (typeof getBehaviourPatterns === 'function')
420    ? getBehaviourPatterns(_ccxEditState.clientId) : [];
421
422  try {
423    var res = await workerPost('https://corefitness-analyse.patrick-hargrave.workers.dev/suggest-context', {
424      client_name: _ccxEditState.clientName,
425      constraints: _ccxEditState.constraints,
426      motivators:  _ccxEditState.motivators,
427      coach_notes: _ccxEditState.coach_notes,
428      patterns: (patterns || []).map(function(p) { return { type: p.type, why: p.why || '' }; })
429    });
430
431    var raw  = await res.text();
432    var data;
433    try { data = JSON.parse(raw); } catch(e) { throw new Error('Bad response from server'); }
434    if (!res.ok || data.error) throw new Error(data.error || 'AI request failed');
435
436    var suggestions = data.suggestions || [];
437    if (suggestions.length === 0) {
438      panel.innerHTML =
439        '<div style="font-size:12px;color:#555;padding:8px 0;text-align:center;">' +
440          'No additional context suggestions based on the current data.' +
441        '</div>';
442    } else {
443      var sugHtml =
444        '<div style="background:#0d1a0d;border:1px solid #1e3a1e;border-radius:8px;padding:12px 14px;margin-top:4px;">' +
445          '<div style="font-size:9px;color:#3ecf8e;letter-spacing:1.5px;text-transform:uppercase;font-family:\'DM Mono\',monospace;margin-bottom:10px;opacity:0.8;">AI Suggestions - tap Use to pre-fill, then save</div>';
446      suggestions.forEach(function(s) {
447        var safeSection = (s.section || '').replace(/['"\\]/g, '');
448        var safeText    = (s.text    || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/[\r\n]/g, ' ');
449        var sectionLabel = s.section === 'constraints' ? 'Constraint' : s.section === 'motivators' ? 'Motivator' : 'Coach Note';
450        var sectionColor = s.section === 'constraints' ? '#e31c1c' : s.section === 'motivators' ? '#f0a84e' : '#4ec9f0';
451        sugHtml +=
452          '<div style="margin-bottom:10px;padding:9px 12px;background:#111;border:1px solid #1e1e1e;border-radius:8px;">' +
453            '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;">' +
454              '<span style="font-size:9px;color:' + sectionColor + ';letter-spacing:1px;text-transform:uppercase;">' + escapeHtml(sectionLabel) + '</span>' +
455              '<button onclick="ccxApplySuggestion(\'' + safeSection + '\',\'' + safeText + '\')" ' +
456                'style="background:#1a2a1a;border:1px solid #3ecf8e44;color:#3ecf8e;border-radius:6px;padding:3px 10px;font-size:10px;cursor:pointer;font-family:inherit;">Use</button>' +
457            '</div>' +
458            '<div style="font-size:12px;color:#aaa;line-height:1.5;">' + escapeHtml(s.text || '') + '</div>' +
459            (s.reason ? '<div style="font-size:10px;color:#444;margin-top:4px;">' + escapeHtml(s.reason) + '</div>' : '') +
460          '</div>';
461      });
462      sugHtml += '</div>';
463      panel.innerHTML = sugHtml;
464    }
465
466    btn.textContent = 'AI Suggestions';
467    btn.disabled    = false;
468  } catch(e) {
469    panel.innerHTML =
470      '<div style="font-size:12px;color:#e31c1c;padding:8px 0;">' +
471        'Could not get suggestions: ' + escapeHtml(e.message) +
472      '</div>';
473    btn.textContent = 'AI Suggestions';
474    btn.disabled    = false;
475  }
476}
477
478function ccxApplySuggestion(section, text) {
479  if (!_ccxEditState) return;
480  if (section === 'coach_notes') {
481    var notesEl = document.getElementById('ccxNotesArea');
482    if (notesEl) {
483      notesEl.value = notesEl.value ? notesEl.value.trim() + '\n' + text : text;
484      notesEl.focus();
485    }
486  } else {
487    var input = document.getElementById('ccxInput_' + section);
488    if (input) { input.value = text; input.focus(); }
489  }
490}

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.