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.