/** * capture.js — client-side session-context capture for the Feedback feature. * * Exposes window.FeedbackCapture with pure capture primitives (no network, no * app-specific closure state). The console wires these into its submit/flush * flow and pushes on-screen events into the ring buffer at its render points. * * FeedbackCapture.sessionId — stable per-tab session id * FeedbackCapture.pushLog(entry) — record one on-screen event * FeedbackCapture.getLogJSON() — serialize the buffer to a JSON string * FeedbackCapture.clearLog() — reset the buffer * FeedbackCapture.buildSnapshot() — Promise single self-contained page * * Interaction log scope (FR-014): callers push ONLY content that is visible on * screen. This module never reads tokens, keys, or storage — it only holds what * it is handed. */ (function () { 'use strict'; var SESSION_KEY = 'feedback_session_id'; var MAX_ENTRIES = 500; // bound memory for long / continuous sessions function _uuid() { try { return crypto.randomUUID(); } catch (_) { return 'sess-' + Date.now() + '-' + Math.random().toString(16).slice(2); } } var sessionId; try { sessionId = sessionStorage.getItem(SESSION_KEY) || _uuid(); sessionStorage.setItem(SESSION_KEY, sessionId); } catch (_) { sessionId = _uuid(); } var _buffer = []; /** * Record one on-screen event. * @param {{role?:string, kind?:string, text?:string}} entry * role: 'user' | 'agent' | 'system' * kind: 'input' | 'output' | 'progress' | 'step' | 'error' */ function pushLog(entry) { entry = entry || {}; var text = entry.text == null ? '' : String(entry.text); if (text.length > 4000) text = text.slice(0, 4000) + '…'; _buffer.push({ ts: new Date().toISOString(), role: entry.role || 'system', kind: entry.kind || 'output', text: text }); if (_buffer.length > MAX_ENTRIES) _buffer.splice(0, _buffer.length - MAX_ENTRIES); } function getLogJSON() { return JSON.stringify(_buffer); } function getLogCount() { return _buffer.length; } function clearLog() { _buffer = []; } // Selectors whose content the console hides behind a click: collapsed finding // groups, unselected tab panes, and panels that only appear once a step // finishes. A snapshot exists to show a reviewer WHAT THE USER GOT, and the // generated artifacts — the findings by category, the refreshed post, the // before/after review — all live behind one of these. They are present in the // DOM either way, but a raw clone opens collapsed, so the reviewer sees an // empty shell and concludes nothing was generated. // // Each entry adds `cls` to every element matching `sel`, which is exactly what // the console's own click handlers do. Keep this list in sync with the CSS in // console.html (.out/.show, .fgroup/.open, .tabpane/.show, .intensity/.show). var EXPAND_RULES = [ { sel: '.out', cls: 'show' }, // step panels (scan results, refine results, feedback) { sel: '.fgroup', cls: 'open' }, // finding groups: broken links, SEO, AEO, GEO, … { sel: '.tabpane', cls: 'show' }, // link swaps / refreshed post / full review { sel: '.intensity', cls: 'show' } ]; // Elements the console toggles with an INLINE display:none. Cleared by id, not // blanket-cleared: #paywallModal is also inline-hidden and is a full-screen // fixed overlay — un-hiding that would black out the whole snapshot. var UNHIDE_IDS = ['postMd', 'postRender', 'refineTabs']; var SNAPSHOT_STYLE = [ '/* injected by capture.js — force every collapsible open in the snapshot */', '.out,.tabpane,.intensity{display:block!important}', '.fgbody{display:block!important}', '.tabs{display:flex!important}', 'details{display:block!important}', '.snapshot-banner{font:600 12px/1.5 system-ui,sans-serif;background:#1d3b12;', 'color:#c8f560;padding:8px 14px;border-bottom:1px solid #2f5c1e}' ].join('\n'); /** * Reveal, in the CLONE only, everything the live page keeps one click away. * Never touches the live DOM — the user's screen is left exactly as it was. */ function _expandCollapsed(clone) { var i, els; for (i = 0; i < EXPAND_RULES.length; i++) { els = clone.querySelectorAll(EXPAND_RULES[i].sel); for (var j = 0; j < els.length; j++) els[j].classList.add(EXPAND_RULES[i].cls); } els = clone.querySelectorAll('details'); for (i = 0; i < els.length; i++) els[i].setAttribute('open', ''); for (i = 0; i < UNHIDE_IDS.length; i++) { var el = clone.querySelector('#' + UNHIDE_IDS[i]); if (el && el.style && el.style.display === 'none') el.style.display = ''; } // A