您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
🧠 Local-only AI that learns your Instagram preferences: robust watch-time tracking, AB-learning, overlays, filters, snapshots, per-media tuning, debug tools, creator overexposure dampener, and a modern responsive UI (ripple, tooltips, toggles, quick actions). Privacy-first, standalone, and polished.
当前为
// ==UserScript== // @name 🌟 Instagram Preference AI — Private Insights // @namespace https://gf.qytechs.cn/en/scripts/548510-instagram-preference-ai-private-insights // @version 3.1.1 // @description 🧠 Local-only AI that learns your Instagram preferences: robust watch-time tracking, AB-learning, overlays, filters, snapshots, per-media tuning, debug tools, creator overexposure dampener, and a modern responsive UI (ripple, tooltips, toggles, quick actions). Privacy-first, standalone, and polished. // @author You // @license MIT // @match https://www.instagram.com/* // @match https://instagram.com/* // @match https://m.instagram.com/* // @icon https://www.instagram.com/static/images/ico/favicon-192.png/68d99ba29cc8.png // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (() => { 'use strict'; /************************************************************************************************* * IMPORTANT * - This script does nothing outside your own page. It doesn't auto-like/follow/comment. * - It ONLY observes what’s on screen and your clicks, to personalize sorting hints locally. * - If Instagram changes its HTML structure, open the panel > Settings > Run Self-Test. *************************************************************************************************/ /*************** * 0) HELPERS * ***************/ const SCHEMA_VER = 5; const EPS = 1e-7; const clamp01 = x => Math.min(1, Math.max(0, x)); const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x)); const sigmoid = z => z >= 0 ? 1 / (1 + Math.exp(-z)) : Math.exp(z) / (1 + Math.exp(z)); const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); const sleep = ms => new Promise(r => setTimeout(r, ms)); const randInt = n => (Math.random() * n) | 0; const log = (...a) => console.debug('[IG-PA Ultra]', ...a); const warn = (...a) => console.warn('[IG-PA Ultra]', ...a); function gmGet(k, def){ try { if (typeof GM_getValue === 'function') return GM_getValue(k, def); } catch {} try { const raw = localStorage.getItem('IGPA_ULTRA_'+k); return raw ? JSON.parse(raw) : def; } catch { return def; } } function gmSet(k, v){ try { if (typeof GM_setValue === 'function') return GM_setValue(k, v); } catch {} try { localStorage.setItem('IGPA_ULTRA_'+k, JSON.stringify(v)); } catch {} } function addStyle(css){ try { if (typeof GM_addStyle === 'function') GM_addStyle(css); else { const s=document.createElement('style'); s.textContent=css; document.head.appendChild(s); } } catch {} } /**************** * 1) CONFIG * ****************/ const CONFIG = { version: '3.1.1', ui: { zIndex: 2147482000, fabSize: 56, defaultPanelWidth: clamp(Math.floor(window.innerWidth * 0.52), 420, 820), defaultPanelHeight: clamp(Math.floor(window.innerHeight * 0.70), 520, 900), minPanelWidth: 380, minPanelHeight: 420, badgeRadius: 12, accent: '#58c7fa' }, learn: { // Dual-learner A/B; best arm is championed periodically. lr: 0.11, lrA: 0.11, lrB: 0.20, abPeriod: 120, // Watch/exposure → continuous labels. minNegWatchSec: 1.0, // <1s is strongly negative minPosWatchSec: 6.0, // ≥6s is strongly positive maxWatchImpactSec: 40, // saturation ceiling for normalization wImage: 1.00, wVideo: 1.20, clickBoost: 0.18, decay: 0.0006, maxEvents: 10000, vocab: { maxHashtags: 300, maxCreators: 360, maxStems: 900 }, overexposureWindow: 14, overexposurePenalty: 0.12 }, uiDefaults: { theme: 'auto', // auto|dark|light showBadge: true, compactBadge: false, showExplain: true, dimLow: true, blurLow: false, lowThr: 0.40, highThr: 0.82, eyeComfort: true, capCreator: true, creatorCap: 1.14, autoSkip: false, autoSkipDelay: 760, autoSkipJitter: 300, pauseLearning: false, showDebug: false, showHUD: true } }; // DOM selectors — permissive, because IG mutates often. const SEL = { article: 'article, div[role="article"]', postAnchor: 'a[href*="/p/"], a[href*="/reel/"], a[href*="/reels/"]', video: 'video', usernameAnchor: 'header a[href^="/"], a[role="link"][href^="/"]', captionCandidates: ['h1','h2','span','div[role="button"]','li','div'] }; /***************** * 2) TOKENIZER * *****************/ const STOP = new Set(('a an the and or but if then else than when where who why how to in on at from for with by of as is are was were be been being this that these those i you he she it we they them me my your our their not no do does did doing so such very just over under into out up down more most less few many each any some only own same').split(' ')); const stem = w => w.replace(/(ing|ers|er|ies|ied|iness|ness|ments|ment|ation|ations)$/,'').replace(/(ed|ly|es|s)$/,'').replace(/(.)\1{2,}/g,'$1'); function tokenize(text){ return (text||'').toLowerCase() .replace(/https?:\/\/\S+|[@#]\w+/g,' ') .replace(/[^a-z0-9\s]/g,' ') .split(/\s+/) .filter(w => w && !STOP.has(w) && w.length>=3 && w.length<=24) .map(stem); } const extractHashtags = text => { const s=new Set(); if (!text) return []; text.replace(/#([0-9A-Za-z_]+)/g,(_,t)=>{ if (t) s.add(t.toLowerCase()); return ''; }); return [...s]; }; /**************** * 3) STATE * ****************/ const DEFAULT_STATE = { schema: SCHEMA_VER, ui: {...CONFIG.uiDefaults}, weights: { __bias: 0 }, locks: {}, stats: { events: 0, positives: 0, negatives: 0, updatedAt: Date.now(), imgSec:0, vidSec:0 }, pins: { creators: {}, hashtags: {}, keywords: {} }, bans: { creators: {}, hashtags: {}, keywords: {} }, mutedHashtags: {}, snooze: { creators: {} }, history: [], // { ts,id,label,seconds,feats,via,pred,loss,arm,lr,creator } ab: { aLoss:0, bLoss:0, count:0, armNext:'A' }, colorCache: {}, snapshots: {}, status: { booted:false, observing:false, lastError:'' }, panel: { x:null, y:null, w:null, h:null, minimized:false }, adapt: { lowStreak:0, baseLow: CONFIG.uiDefaults.lowThr }, hud: { x:null, y:null }, themeAccent: CONFIG.ui.accent }; let STATE = migrate(Object.assign({}, DEFAULT_STATE, gmGet('state_ultra_stable', DEFAULT_STATE))); function migrate(s){ try{ if (!s || typeof s!=='object') return JSON.parse(JSON.stringify(DEFAULT_STATE)); if (!('schema' in s)) s.schema = 1; // v5 fields: if (s.schema < 5){ s.themeAccent = s.themeAccent || CONFIG.ui.accent; if (!s.ui) s.ui = {...CONFIG.uiDefaults}; if (s.ui.lowScoreThreshold!=null){ s.ui.lowThr = s.ui.lowScoreThreshold; delete s.ui.lowScoreThreshold; } if (s.ui.highScoreThreshold!=null){ s.ui.highThr = s.ui.highScoreThreshold; delete s.ui.highScoreThreshold; } s.schema = 5; } return s; } catch { return JSON.parse(JSON.stringify(DEFAULT_STATE)); } } function persist(){ gmSet('state_ultra_stable', STATE); } /******************* * 4) POST PARSING * *******************/ function hashStr(str){ let h=5381; for(let i=0;i<str.length;i++) h=((h<<5)+h)+str.charCodeAt(i); return (h>>>0).toString(36); } function getPostIdFromEl(el){ try{ const a = el.querySelector(SEL.postAnchor); if (a){ const href = a.getAttribute('href') || ''; const m = href.match(/\/(p|reel|reels)\/([^\/?#]+)\//); if (m) return `${m[1]}:${m[2]}`; return href; } const dataId = el.getAttribute('data-testid') || el.getAttribute('data-post-id'); if (dataId) return 'd:'+dataId; return 'x:'+hashStr((el.textContent||'').slice(0,1000)); } catch { return 'x:'+Math.random().toString(36).slice(2); } } function isPost(el){ if (!(el instanceof HTMLElement)) return false; const a = el.querySelector(SEL.postAnchor); const hasVid = !!el.querySelector(SEL.video); const hasImg = !!el.querySelector('img'); const roleArticle = el.matches(SEL.article); return !!(a || hasVid || (roleArticle && hasImg)); } function getUsername(el){ try{ const h = el.querySelector(SEL.usernameAnchor); if (!h) return null; const href = h.getAttribute('href') || ''; const m = href.match(/^\/([^\/?#]+)\/?$/); return m ? m[1].toLowerCase() : null; } catch { return null; } } function getCaptionText(el){ let best = ''; try { for (const sel of SEL.captionCandidates) { const nodes = el.querySelectorAll(sel); for (const node of nodes) { const txt = (node.textContent||'').trim(); if (!txt) continue; const hashCount = (txt.match(/#\w/g)||[]).length; const bestHash = (best.match(/#\w/g)||[]).length; if (hashCount > bestHash) best = txt; else if (txt.length > best.length && txt.length < 4000) best = txt; } if (best) break; } } catch {} return best.trim(); } function getPostType(el){ try{ if (el.querySelector(SEL.video)) return 'video'; const imgs = el.querySelectorAll('img'); if (imgs && imgs.length >= 2) return 'carousel'; return 'image'; } catch { return 'image'; } } function captionBucket(len){ return len<40?'cap:short' : len<140?'cap:med' : 'cap:long'; } const SPONSORED_RE = /\b(sponsored|advert(?:isement|orial)?|paid (?:partnership|collab(?:oration)?)|promoted|partnered)\b/i; const likelySponsored = el => !!el && SPONSORED_RE.test((el.innerText||'').toLowerCase()); /************************* * 5) FEATURE GENERATION * *************************/ function timeFeats(ts=Date.now()){ const d=new Date(ts), h=d.getHours(), w=d.getDay(); const out={}; out['tod:'+(h<6?'night':h<12?'morning':h<18?'afternoon':'evening')] = 1; out['dow:'+['sun','mon','tue','wed','thu','fri','sat'][w]] = 1; out[w===0||w===6?'weekend':'weekday'] = 1; return out; } function overexposurePen(creator){ try{ const N = CONFIG.learn.overexposureWindow; const recent = STATE.history.slice(-N); let c=0; for (const ev of recent){ if ((ev?.creator||'')===creator) c++; } if (c <= Math.floor(N*0.4)) return 0; return Math.min(1, c/Math.max(1, N)) * CONFIG.learn.overexposurePenalty; } catch { return 0; } } function featuresFromPost(el, extra={}){ const feats = {}; const type = extra.type || getPostType(el); const creator = (extra.creator || getUsername(el) || 'unknown'); const caption = extra.caption || getCaptionText(el); const tags = extractHashtags(caption).filter(t=>t.length<=32); const toks = tokenize(caption).slice(0, 40); feats['type:'+type] = 1; feats['user:'+creator] = 1; tags.forEach(t => feats['tag:'+t] = 1); toks.forEach(s => feats['kw:'+s] = 1); feats[captionBucket(caption.length)] = 1; feats['has:hashtags'] = tags.length>0 ? 1 : 0; feats['has:video'] = (type==='video') ? 1 : 0; feats['has:carousel'] = (type==='carousel') ? 1 : 0; if (STATE.ui.eyeComfort) feats['flag:sponsored'] = likelySponsored(el) ? 1 : 0; feats['flag:mutedTag'] = tags.some(t=>STATE.mutedHashtags[t]) ? 1 : 0; // Watch buckets if (extra.watchBucket) feats['watch:'+extra.watchBucket] = 1; if (extra.playHeavy) feats['watch:playHeavy'] = 1; if (extra.exposureHeavy) feats['watch:exposureHeavy'] = 1; if (extra.carouselSlides && extra.carouselSlides>=3) feats['carousel:slides>=3'] = 1; // Time/context Object.assign(feats, timeFeats(extra.ts||Date.now())); // Dampeners const pen = overexposurePen(creator); if (pen>0) feats['reg:overexposed'] = pen; if (STATE.snooze?.creators?.[creator] && Date.now()<STATE.snooze.creators[creator]) feats['reg:snoozed'] = 1.25; return feats; } function dot(weights, feats){ let z = (weights.__bias || 0); let creatorKey=null; for (const k in feats){ if (k.startsWith('user:')){ creatorKey = k; break; } } for (const k in feats){ let w = (weights[k] || 0); const v = feats[k]; if (STATE.ui.capCreator && creatorKey && k===creatorKey){ const cap = STATE.ui.creatorCap; if (w > cap) w = cap; if (w < -cap) w = -cap; } if (k.startsWith('tag:')){ const tag=k.slice(4); if (STATE.pins.hashtags[tag]) w += 0.25; if (STATE.bans.hashtags[tag]) w -= 0.35; } if (k.startsWith('kw:')){ const kw=k.slice(3); if (STATE.pins.keywords[kw]) w += 0.18; if (STATE.bans.keywords[kw]) w -= 0.25; } if (k.startsWith('user:')){ const u=k.slice(5); if (STATE.pins.creators[u]) w += 0.25; if (STATE.bans.creators[u]) w -= 0.40; if (STATE.snooze?.creators?.[u] && Date.now()<STATE.snooze.creators[u]) w -= 1.2; } z += w * v; } return z; } const predict = (el, feats=null) => sigmoid(dot(STATE.weights, feats || featuresFromPost(el))); /*********************** * 6) LEARNING + A/B * ***********************/ function applyDecay(){ try{ const w=STATE.weights, d=CONFIG.learn.decay; for (const k in w) if (k!=='__bias') w[k]*=(1-d); } catch(e){ STATE.status.lastError = 'decay:'+String(e); } } function softPrune(w, prefix, max){ try{ const keys = Object.keys(w).filter(k=>k.startsWith(prefix)); if (keys.length <= max) return; keys.sort((a,b)=>Math.abs(w[a])-Math.abs(w[b])); const drop = Math.min(keys.length - max, Math.max(1, Math.floor(keys.length*0.12))); for (let i=0;i<drop;i++) if (!STATE.locks[keys[i]]) delete w[keys[i]]; } catch(e){ STATE.status.lastError = 'prune:'+String(e); } } function sgd(feats, label){ if (STATE.ui.pauseLearning) return { pred: sigmoid(dot(STATE.weights, feats)), loss:0, arm:'P', lrUsed:0 }; const arm = (STATE.ab.armNext==='A')?'A':'B'; const lr = (arm==='A')?CONFIG.learn.lrA:CONFIG.learn.lrB; const pred = sigmoid(dot(STATE.weights, feats)); const loss = -(label ? Math.log(clamp01(pred)+EPS) : Math.log(clamp01(1-pred)+EPS)); const err = (label - pred); const w = STATE.weights; w.__bias = (w.__bias || 0) + lr * err; for (const k in feats){ if (STATE.locks[k]) continue; w[k] = (w[k] || 0) + lr * err * feats[k]; } softPrune(w, 'kw:', CONFIG.learn.vocab.maxStems); softPrune(w, 'tag:', CONFIG.learn.vocab.maxHashtags); softPrune(w, 'user:', CONFIG.learn.vocab.maxCreators); applyDecay(); if (arm==='A') STATE.ab.aLoss += loss; else STATE.ab.bLoss += loss; STATE.ab.count++; STATE.ab.armNext = arm==='A' ? 'B':'A'; if (STATE.ab.count >= CONFIG.learn.abPeriod){ const win = (STATE.ab.aLoss <= STATE.ab.bLoss) ? 'A' : 'B'; CONFIG.learn.lr = (win==='A') ? CONFIG.learn.lrA : CONFIG.learn.lrB; STATE.ab = { aLoss:0, bLoss:0, count:0, armNext:(win==='A'?'B':'A') }; } STATE.stats.events++; STATE.stats.updatedAt = Date.now(); return { pred, loss, arm, lrUsed: lr }; } /*************************** * 7) WATCH-TIME & LABELS * ***************************/ const activeTimers = new Map(); // id -> { el, start, accum, autoskipTimer, skipped, slides } const videoTrackers = new Map(); // video -> { id, playAccum, lastTick, playing, cleanup } const watchBucket = sec => sec<2 ? 'tiny' : sec<5 ? 'short' : sec<10 ? 'med' : sec<20 ? 'long' : 'vlong'; function normWatch(sec, type){ const x = clamp01(sec/CONFIG.learn.maxWatchImpactSec); const smooth = x*x*(3-2*x); // smoothstep let label = (sec<CONFIG.learn.minNegWatchSec) ? 0 : (sec>=CONFIG.learn.minPosWatchSec) ? 1 : smooth; label *= (type==='video') ? CONFIG.learn.wVideo : CONFIG.learn.wImage; return clamp01(label); } // IntersectionObserver for exposure time (images & carousels) let io = null; function ensureIO(){ if (io) return io; io = new IntersectionObserver(entries=>{ for (const entry of entries){ const el = entry.target; if (!isPost(el)) continue; const id = getPostIdFromEl(el); const rec = activeTimers.get(id) || { el, start:null, accum:0, autoskipTimer:null, skipped:false, slides:1 }; rec.el = el; if (entry.isIntersecting && entry.intersectionRatio >= 0.55){ if (!rec.start) rec.start = now(); rec.slides = trackCarouselSlides(el, rec.slides); scheduleAutoSkip(el, id, rec); attachVideoTracker(el, id); } else { if (rec.start){ rec.accum += (now()-rec.start)/1000; rec.start = null; onExposureComplete(el, id, rec.accum, rec.slides); rec.accum = 0; rec.slides = 1; rec.skipped = false; } if (rec.autoskipTimer){ clearTimeout(rec.autoskipTimer); rec.autoskipTimer=null; } } activeTimers.set(id, rec); } }, { threshold:[0,0.55,1] }); return io; } // Carousel heuristic: when first <img> src changes, treat as slide change. const lastImgSrc = new WeakMap(); function trackCarouselSlides(el, prev=1){ if (getPostType(el)!=='carousel') return prev; const img = el.querySelector('img'); if (!img || !img.src) return prev; const last = lastImgSrc.get(el); if (last && last !== img.src) prev = Math.min(12, prev+1); lastImgSrc.set(el, img.src); return prev; } // Video trackers function attachVideoTracker(el, id){ try{ el.querySelectorAll(SEL.video).forEach(v=>{ if (videoTrackers.has(v)) return; const tr = { id, playAccum:0, lastTick:0, playing:false, cleanup:null }; const onPlay = ()=>{ tr.playing=true; tr.lastTick=now(); }; const onPause= ()=>{ if (tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.playing=false; } }; const onTime = ()=>{ if (tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.lastTick=now(); } }; const onEnd = ()=>{ onPause(); flushVideo(v,tr); }; v.addEventListener('play',onPlay); v.addEventListener('pause',onPause); v.addEventListener('timeupdate',onTime); v.addEventListener('ended',onEnd); tr.cleanup = ()=>{ v.removeEventListener('play',onPlay); v.removeEventListener('pause',onPause); v.removeEventListener('timeupdate',onTime); v.removeEventListener('ended',onEnd); }; videoTrackers.set(v,tr); }); } catch(e){ STATE.status.lastError='video:'+String(e); } } function flushVideo(v, tr){ try{ if (!tr) tr = videoTrackers.get(v); if (!tr) return; if (tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.playing=false; } const rec = activeTimers.get(tr.id); const el = rec?.el || v.closest(SEL.article); if (el) onVideoComplete(el, tr.id, tr.playAccum); tr.playAccum = 0; } catch {} } function onExposureComplete(el, id, seconds, slides){ try{ if (!el || getPostType(el)==='video') return; // videos handled separately STATE.stats.imgSec += seconds; const nlabel = normWatch(seconds * (slides>=3?1.15:1), 'image'); const label = seconds<CONFIG.learn.minNegWatchSec ? 0 : nlabel; const feats = featuresFromPost(el, { watchBucket:watchBucket(seconds), exposureHeavy:1, carouselSlides:slides, ts:Date.now() }); const upd = sgd(feats, label); const creator = getUsername(el) || 'unknown'; recordEvent({ ts:Date.now(), id, creator, label, seconds, via:'exposure', pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed, feats:pickTop(feats) }); paintBadge(el); } catch(e){ STATE.status.lastError='exp:'+String(e); } } function onVideoComplete(el, id, seconds){ try{ if (!el || seconds<0.01) return; STATE.stats.vidSec += seconds; const nlabel = normWatch(seconds, 'video'); const label = seconds<CONFIG.learn.minNegWatchSec ? 0 : nlabel; const feats = featuresFromPost(el, { watchBucket:watchBucket(seconds), playHeavy:1, ts:Date.now() }); const upd = sgd(feats, label); const creator = getUsername(el) || 'unknown'; recordEvent({ ts:Date.now(), id, creator, label, seconds, via:'video', pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed, feats:pickTop(feats) }); paintBadge(el); } catch(e){ STATE.status.lastError='vid:'+String(e); } } // Like/Save → boosted label function wasLiked(el){ try{ const btn = el.querySelector('button[aria-label*="Like" i], svg[aria-label*="Like" i]'); if (!btn) return false; const pressed = btn.getAttribute('aria-pressed'); if (pressed === 'true') return true; const aria=(btn.getAttribute('aria-label')||'').toLowerCase(); return /\bunlike\b|\balready liked\b/.test(aria); }catch{return false;} } function wasSaved(el){ try{ const btn = el.querySelector('button[aria-label*="Save" i], svg[aria-label*="Save" i]'); if (!btn) return false; const pressed = btn.getAttribute('aria-pressed'); if (pressed === 'true') return true; const aria=(btn.getAttribute('aria-label')||'').toLowerCase(); return /\bremove\b|\balready saved\b/.test(aria); }catch{return false;} } document.addEventListener('click', e=>{ try{ const target = e.target; if (!(target instanceof Element)) return; const el = target.closest(SEL.article); if (!el || !isPost(el)) return; // defer a tick to let IG toggle aria states setTimeout(()=>{ const liked = wasLiked(el), saved = wasSaved(el); if (!liked && !saved) return; const id = getPostIdFromEl(el); let sec = 0; const rec = activeTimers.get(id); if (rec?.start){ sec += (now()-rec.start)/1000; rec.start=null; } el.querySelectorAll(SEL.video).forEach(v=>{ const tr = videoTrackers.get(v); if (!tr) return; if (tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.playing=false; } sec += tr.playAccum; tr.playAccum=0; }); const base = normWatch(sec, getPostType(el)); const label = clamp01(base + CONFIG.learn.clickBoost); const feats = featuresFromPost(el, { watchBucket:watchBucket(sec) }); const upd = sgd(feats, label); const creator = getUsername(el) || 'unknown'; recordEvent({ ts:Date.now(), id, creator, label, seconds:sec||null, via: liked?'like':'save', pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed, feats:pickTop(feats) }); paintBadge(el); }, 100); } catch (e2){ STATE.status.lastError='click:'+String(e2); } }, true); // Flush timers on pagehide/visibility change function flushTimers(){ const t = now(); for (const [id,rec] of activeTimers){ try{ if (rec.start){ rec.accum += (t-rec.start)/1000; rec.start=null; if (rec.el && document.contains(rec.el)) onExposureComplete(rec.el, id, rec.accum, rec.slides||1); rec.accum=0; rec.slides=1; } rec.el?.querySelectorAll?.(SEL.video).forEach(v=>{ const tr=videoTrackers.get(v); if (tr) flushVideo(v,tr); }); }catch{} } } window.addEventListener('pagehide', flushTimers); document.addEventListener('visibilitychange', ()=>{ if (document.visibilityState!=='visible') flushTimers(); }); /************************ * 8) AUTOSKIP (OPTION) * ************************/ function effectiveLow(){ const base = (STATE.adapt.baseLow ?? STATE.ui.lowThr); const streak = STATE.history.slice(-10).filter(e=>e.label<0.5).length; const bump = streak >= 6 ? 0.10 : streak >= 3 ? 0.05 : 0; return clamp01(base + bump); } function scheduleAutoSkip(el, id, rec){ if (!STATE.ui.autoSkip || rec.autoskipTimer || rec.skipped) return; const delay = STATE.ui.autoSkipDelay + randInt(STATE.ui.autoSkipJitter); rec.autoskipTimer = setTimeout(()=>{ try{ const s = predict(el); if (s < effectiveLow()){ rec.skipped = true; const r = el.getBoundingClientRect(); window.scrollBy({ top: Math.max(160, r.height+36), behavior:'smooth' }); } } finally { rec.autoskipTimer = null; } }, delay); } /******************** * 9) UI FOUNDATION * ********************/ addStyle(` :root { --igpa-blue: #0095f6; --igpa-red: #ed4956; --igpa-green: #58c322; --igpa-dark: #121212; --igpa-light: #ffffff; --igpa-gray-light: #efefef; --igpa-gray-dark: #262626; --igpa-border-light: #dbdbdb; --igpa-border-dark: #2c2c2c; --igpa-gradient: linear-gradient(45deg,#f58529,#dd2a7b,#8134af,#515bd4); } [data-igpa-theme="light"] { --igpa-bg: var(--igpa-light); --igpa-text: var(--igpa-gray-dark); --igpa-border: var(--igpa-border-light); } [data-igpa-theme="dark"] { --igpa-bg: var(--igpa-dark); --igpa-text: var(--igpa-light); --igpa-border: var(--igpa-border-dark); } * { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important; -webkit-font-smoothing: antialiased; } /* Floating Action Button */ .igpa-fab { position: fixed; right: 20px; bottom: 20px; width: 56px; height: 56px; border-radius: 50%; background: var(--igpa-gradient); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2147482000; box-shadow: 0 4px 12px rgba(0,0,0,.3); transition: transform .2s ease; } .igpa-fab:hover { transform: scale(1.08); } .igpa-fab svg { fill: #fff; width: 26px; height: 26px; } /* Panel */ .igpa-panel { position: fixed; right: 16px; bottom: 80px; width: 420px; max-height: 72%; background: var(--igpa-bg); color: var(--igpa-text); border: 1px solid var(--igpa-border); border-radius: 18px; box-shadow: 0 8px 28px rgba(0,0,0,.2); backdrop-filter: blur(12px); overflow: hidden; display: none; flex-direction: column; font-size: 14px; line-height: 1.45; animation: igpa-fade .25s ease-out; } .igpa-panel.show { display: flex; } @keyframes igpa-fade { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .igpa-title { font-weight: 600; font-size: 15px; padding: 14px 16px; border-bottom: 1px solid var(--igpa-border); display: flex; align-items: center; justify-content: space-between; } /* Tabs */ .igpa-tabs { display: flex; gap: 6px; padding: 10px 14px; border-bottom: 1px solid var(--igpa-border); } .igpa-tab { padding: 6px 14px; border-radius: 999px; border: 1px solid var(--igpa-border); font-size: 13px; font-weight: 500; cursor: pointer; background: transparent; transition: background .2s, color .2s; } .igpa-tab.active { background: var(--igpa-blue); border-color: var(--igpa-blue); color: #fff; } /* Buttons */ .igpa-btn { padding: 7px 16px; border-radius: 999px; border: 1px solid var(--igpa-border); font-size: 14px; font-weight: 500; cursor: pointer; background: transparent; transition: background .2s, transform .15s; } .igpa-btn:hover { transform: translateY(-1px); } .igpa-btn--primary { background: var(--igpa-blue); color: #fff; border-color: var(--igpa-blue); } .igpa-btn--danger { background: var(--igpa-red); color: #fff; border-color: var(--igpa-red); } /* Chips */ .igpa-chip { display: inline-flex; align-items: center; border-radius: 999px; padding: 4px 10px; font-size: 13px; font-weight: 500; border: 1px solid var(--igpa-border); background: var(--igpa-bg); margin: 2px; } /* Badge (like Instagram "LIVE" tag) */ .igpa-badge { position: absolute; top: 8px; right: 8px; border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .3px; text-transform: uppercase; padding: 3px 8px; background: rgba(0,0,0,.65); color: #fff; backdrop-filter: blur(4px); } [data-igpa-theme="light"] .igpa-badge { background: rgba(255,255,255,.9); color: #262626; } /* HUD (story-style bar) */ .igpa-hud { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); background: var(--igpa-bg); border: 1px solid var(--igpa-border); border-radius: 12px; padding: 6px 12px; font-size: 12px; font-weight: 500; display: flex; flex-direction: column; align-items: center; box-shadow: 0 2px 8px rgba(0,0,0,.15); z-index: 2147482000; } .igpa-hud .bar { width: 100%; height: 3px; border-radius: 3px; background: var(--igpa-border); margin-top: 4px; overflow: hidden; } .igpa-hud .fill { height: 100%; background: var(--igpa-gradient); width: 0%; transition: width .4s ease; } /* Quick Action Buttons */ .igpa-foot { position: absolute; left: 8px; bottom: 8px; display: flex; gap: 8px; } .igpa-qbtn { width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 1px solid var(--igpa-border); background: rgba(255,255,255,.95); color: #262626; cursor: pointer; transition: background .2s; } .igpa-qbtn:hover { background: rgba(0,0,0,.08); } [data-igpa-theme="dark"] .igpa-qbtn { background: rgba(0,0,0,.7); color: #f5f5f5; } [data-igpa-theme="dark"] .igpa-qbtn:hover { background: rgba(255,255,255,.1); } `); // Icons function icon(path, view='0 0 24 24'){ const s=document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('viewBox',view); s.style.width='16px'; s.style.height='16px'; const p=document.createElementNS('http://www.w3.org/2000/svg','path'); p.setAttribute('d',path); s.appendChild(p); return s; } function mkBtn(txt='', variant='', {iconEl=null, id=null}={}){ const b=document.createElement('button'); b.className='igpa-btn'+(variant?(' '+variant):''); b.type='button'; if (id) b.id = id; if (iconEl){ b.appendChild(iconEl); if (txt){ const sp=document.createElement('span'); sp.textContent=' '+txt; b.appendChild(sp); } } else b.textContent = txt; return b; } // HUD let hud=null; function buildHUD(){ if (hud || !STATE.ui.showHUD) return; hud=document.createElement('div'); hud.className='igpa-hud'; hud.innerHTML = `<div><strong>IG-PA</strong> <span class="igpa-small">pos ratio</span></div><div class="bar"><div class="fill"></div></div><div class="igpa-small"><span data-hud="ratio">0%</span> • events <span data-hud="events">0</span></div>`; document.documentElement.appendChild(hud); // drag let dragging=false,sx=0,sy=0,px=0,py=0; hud.addEventListener('mousedown',e=>{ dragging=true; sx=e.clientX; sy=e.clientY; const r=hud.getBoundingClientRect(); px=r.left; py=r.top; e.preventDefault(); }); document.addEventListener('mousemove',e=>{ if(!dragging) return; hud.style.left=(px+(e.clientX-sx))+'px'; hud.style.top=(py+(e.clientY-sy))+'px'; hud.style.right='auto'; hud.style.bottom='auto'; }); document.addEventListener('mouseup',()=>{ if(!dragging) return; dragging=false; const r=hud.getBoundingClientRect(); STATE.hud.x=r.left; STATE.hud.y=r.top; persist(); }); if (STATE.hud.x!=null) hud.style.left=STATE.hud.x+'px'; if (STATE.hud.y!=null) hud.style.top=STATE.hud.y+'px'; updateHUD(); } function removeHUD(){ hud?.remove(); hud=null; } function updateHUD(){ if (!hud) return; const last = STATE.history.slice(-200); const pos = last.reduce((a,e)=>a+(e.label>=0.5?1:0),0); const ratio = last.length ? (pos/last.length) : 0; const ratioEl=hud.querySelector('[data-hud="ratio"]'); const evEl=hud.querySelector('[data-hud="events"]'); const fill=hud.querySelector('.fill'); if (ratioEl) ratioEl.textContent = Math.round(ratio*100)+'%'; if (evEl) evEl.textContent = String(STATE.stats.events); if (fill) fill.style.width = Math.round(ratio*100)+'%'; } // Theme function applyTheme(){ let t = STATE.ui.theme; if (t==='auto'){ const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; t = dark ? 'dark' : 'light'; } document.documentElement.setAttribute('data-igpa-theme', t==='dark' ? 'dark' : 'light'); document.documentElement.style.setProperty('--igpa-accent', STATE.themeAccent || CONFIG.ui.accent); } /******************** * 10) PANEL / TABS * ********************/ let fab=null, panel=null, bodyEl=null; function buildPanel(){ if (fab && panel) return; // FAB fab=document.createElement('div'); fab.className='igpa-fab'; fab.title='Open Preference AI'; const star=icon('M12 17.3l-5.5 3 1.1-6.3L3 9.8l6.3-.9L12 3l2.7 5.9 6.3.9-4.6 4.2 1.1 6.3z'); star.style.width='24px'; star.style.height='24px'; fab.appendChild(star); fab.addEventListener('click', ()=>togglePanel(true)); document.documentElement.appendChild(fab); // Panel panel=document.createElement('div'); panel.className='igpa-panel'; panel.style.width=(STATE.panel.w||CONFIG.ui.defaultPanelWidth)+'px'; panel.style.height=(STATE.panel.h||CONFIG.ui.defaultPanelHeight)+'px'; if (STATE.panel.x!=null) panel.style.left=STATE.panel.x+'px'; if (STATE.panel.y!=null) panel.style.top=STATE.panel.y+'px'; const title=document.createElement('div'); title.className='igpa-title'; const left=document.createElement('div'); left.innerHTML=`<strong>IG Preference AI — Ultra</strong> <span class="igpa-small">v${CONFIG.version}</span>`; const right=document.createElement('div'); right.style.display='flex'; right.style.gap='8px'; const bUndo=mkBtn('', '', {iconEl:icon('M12 5v4l-4-4 4-4v4a7 7 0 1 1-7 7h2a5 5 0 1 0 5-5z')}); bUndo.title='Undo not available in this stable build'; bUndo.disabled=true; const bNext=mkBtn('Next','igpa-btn--primary',{iconEl:icon('M8 5l8 7-8 7','0 0 24 24')}); bNext.title='Jump to next high score (J)'; bNext.addEventListener('click', ()=>jumpHigh('next')); const bClose=mkBtn('', 'igpa-btn--danger', {iconEl:icon('M6 6l12 12M18 6L6 18')}); bClose.title='Close'; bClose.addEventListener('click', ()=>togglePanel(false)); right.append(bUndo, bNext, bClose); title.append(left, right); const tabs=document.createElement('div'); tabs.className='igpa-tabs'; const tabNames=['Overview','Insights','Session','Weights','Prefs','Settings','Data','Help']; tabNames.forEach((label,i)=>{ const t=mkBtn(label,''); t.classList.add('igpa-tab'); if (i===0) t.classList.add('active'); t.addEventListener('click',()=>{ tabs.querySelectorAll('.igpa-tab').forEach(x=>x.classList.remove('active')); t.classList.add('active'); renderTab(label.toLowerCase()); }); tabs.appendChild(t); }); bodyEl=document.createElement('div'); bodyEl.className='igpa-body'; const res=document.createElement('div'); res.className='igpa-resize'; let resizing=false, rw=0, rh=0, rsx=0, rsy=0; res.addEventListener('mousedown',e=>{ resizing=true; const r=panel.getBoundingClientRect(); rw=r.width; rh=r.height; rsx=e.clientX; rsy=e.clientY; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove',e=>{ if(!resizing) return; const nw=Math.max(CONFIG.ui.minPanelWidth, rw+(e.clientX-rsx)); const nh=Math.max(CONFIG.ui.minPanelHeight, rh+(e.clientY-rsy)); panel.style.width=nw+'px'; panel.style.height=nh+'px'; }); document.addEventListener('mouseup',()=>{ if(!resizing) return; resizing=false; persistPanelPos(); }); // Drag panel let dragging=false,sx=0,sy=0,px=0,py=0; title.addEventListener('mousedown',e=>{ if ((e.target instanceof HTMLElement) && e.target.closest('.igpa-btn')) return; dragging=true; sx=e.clientX; sy=e.clientY; const r=panel.getBoundingClientRect(); px=r.left; py=r.top; e.preventDefault(); }); document.addEventListener('mousemove',e=>{ if(!dragging) return; panel.style.left=(px+(e.clientX-sx))+'px'; panel.style.top=(py+(e.clientY-sy))+'px'; panel.style.right='auto'; panel.style.bottom='auto'; }); document.addEventListener('mouseup',()=>{ if(!dragging) return; dragging=false; persistPanelPos(); }); panel.append(title, tabs, bodyEl, res); document.documentElement.appendChild(panel); // Shortcuts document.addEventListener('keydown',e=>{ const tg=e.target; if (tg && (/input|textarea|select/i).test(tg.tagName)) return; if (!location.hostname.includes('instagram.com')) return; if (e.ctrlKey && e.shiftKey && e.code==='KeyI'){ togglePanel(); e.preventDefault(); } if (e.code==='KeyJ'){ jumpHigh('next'); } if (e.code==='KeyK'){ jumpHigh('prev'); } if (e.code==='KeyH'){ STATE.ui.autoSkip=!STATE.ui.autoSkip; persist(); tip(`Auto-skip ${STATE.ui.autoSkip?'ON':'OFF'}`); } }); // First draw togglePanel(true); renderTab('overview'); } function togglePanel(force){ const show = (force!==undefined) ? force : !panel.classList.contains('show'); panel.classList.toggle('show', show); } function persistPanelPos(){ try{ const r=panel.getBoundingClientRect(); STATE.panel.x=r.left; STATE.panel.y=r.top; STATE.panel.w=r.width; STATE.panel.h=r.height; persist(); } catch {} } function renderTab(name){ if (!bodyEl) return; if (name==='overview'){ const topTags = topN(prefix(STATE.weights,'tag:'),12); const topUsers= topN(prefix(STATE.weights,'user:'),12); const topKWs = topN(prefix(STATE.weights,'kw:'),12); bodyEl.innerHTML = ` <div class="igpa-kv"> <div>Status</div><div>${STATE.status.observing?'Observing ✅':'Idle ⏸️'} ${STATE.status.lastError?`<span class="igpa-small" style="color:#ff5d5d">(${STATE.status.lastError})</span>`:''}</div> <div>Events</div><div>${STATE.stats.events}</div> <div>Labels (≥0.5 / <0.5)</div><div><span class="igpa-chip">${STATE.history.filter(e=>e.label>=0.5).length}</span> <span class="igpa-chip">${STATE.history.filter(e=>e.label<0.5).length}</span></div> <div>Watch-time</div><div><span class="igpa-chip">img ${STATE.stats.imgSec.toFixed(1)}s</span> <span class="igpa-chip">vid ${STATE.stats.vidSec.toFixed(1)}s</span></div> <div>Learning Rates</div><div>${CONFIG.learn.lr.toFixed(2)} (A:${CONFIG.learn.lrA}, B:${CONFIG.learn.lrB})</div> <div>Auto-skip</div><div><span class="igpa-chip">${STATE.ui.autoSkip?'ON':'OFF'}</span></div> <div>HUD</div><div><span class="igpa-chip">${STATE.ui.showHUD?'Visible':'Hidden'}</span></div> </div> <h4>Top Keywords</h4><div>${topKWs.map(([k,w])=>chip(k.slice(3), w)).join('')}</div> <h4>Top Hashtags</h4><div>${topTags.map(([k,w])=>chip('#'+k.slice(4), w)).join('')}</div> <h4>Top Creators</h4><div>${topUsers.map(([k,w])=>chip('@'+k.slice(5), w)).join('')}</div> `; } else if (name==='insights'){ bodyEl.innerHTML = ` <canvas id="igpa-sp1" style="width:100%;height:60px;background:var(--igpa-elev);border:1px solid var(--igpa-border);border-radius:10px"></canvas> <div class="igpa-small" style="margin:6px 0 2px">Recent labels</div> <canvas id="igpa-sp2" style="width:100%;height:60px;background:var(--igpa-elev);border:1px solid var(--igpa-border);border-radius:10px"></canvas> `; drawSparkline('#igpa-sp1', STATE.history.slice(-200).map(e=>e.seconds||0), Math.max(10, ...STATE.history.map(e=>e.seconds||0), 1)); drawSparkline('#igpa-sp2', STATE.history.slice(-200).map(e=>e.label||0), 1); } else if (name==='session'){ const last = STATE.history.slice(-400).reverse(); const search = document.createElement('input'); search.placeholder='Filter by creator, via, id, tag/kw...'; Object.assign(search.style,{width:'100%',padding:'8px',borderRadius:'10px',border:'1px solid var(--igpa-border)',background:'var(--igpa-elev)',color:'var(--igpa-text)'}); const list=document.createElement('div'); list.style.marginTop='8px'; bodyEl.innerHTML=''; bodyEl.append(search, list); const render = (q='')=>{ const ql=q.trim().toLowerCase(); const arr = last.filter(ev=>{ if(!ql) return true; return (ev.creator||'').toLowerCase().includes(ql) || (ev.via||'').toLowerCase().includes(ql) || (ev.id||'').toLowerCase().includes(ql) || JSON.stringify(ev.feats||[]).toLowerCase().includes(ql); }); list.innerHTML = arr.map(ev=>{ const t=new Date(ev.ts).toLocaleTimeString(); const lbl = ev.label>=0.999?'Positive' : ev.label<=0.001?'Negative' : `Label ${ev.label.toFixed(2)}`; const ft=(ev.feats||[]).map(f=>`${pretty(f.k)}:${(f.contrib||0).toFixed(2)}`).join(' · '); const color=ev.label>=0.5?'var(--igpa-positive)':'var(--igpa-danger)'; return `<div class="igpa-row" style="align-items:flex-start;border-bottom:1px solid var(--igpa-border);padding:8px 0"> <div class="igpa-small" style="opacity:.75;min-width:74px">${t}</div> <div style="flex:1"> <span class="igpa-chip">${lbl}</span> <span class="igpa-small">via ${ev.via||'watch'} • @${ev.creator||'-'}</span> <span class="igpa-small">pred ${(ev.pred||0).toFixed(2)}, loss ${(ev.loss||0).toFixed(3)}, arm ${ev.arm||'-'} (lr ${ev.lr?ev.lr.toFixed(2):'-'})</span> <div class="igpa-small" style="margin-top:3px;color:${color}">${ft}</div> </div> <div class="igpa-small" style="min-width:56px;text-align:right">${ev.seconds!=null?`${ev.seconds.toFixed(1)}s`:''}</div> </div>`; }).join('') || '<p class="igpa-small">No events yet — scroll to train the model.</p>'; }; render(); search.oninput = ()=>render(search.value); } else if (name==='weights'){ bodyEl.innerHTML = ` <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;"> <input id="wq" placeholder="Search feature or @creator/#tag/kw" style="flex:1;padding:8px;border-radius:10px;border:1px solid var(--igpa-border);background:var(--igpa-elev);color:var(--igpa-text)"/> <button class="igpa-btn" id="wnorm">Normalize</button> <button class="igpa-btn" id="wshrink">L1 Shrink</button> </div> <div id="wlist"></div> `; const q=bodyEl.querySelector('#wq'); const list=bodyEl.querySelector('#wlist'); const render=()=>{ const s=(q.value||'').toLowerCase(); const items=Object.entries(STATE.weights).filter(([k])=>k!=='__bias' && (s? k.toLowerCase().includes(s):true)).sort((a,b)=>Math.abs(b[1])-Math.abs(a[1])); list.innerHTML = items.slice(0,600).map(([k,w])=>` <div class="igpa-row" data-key="${k}"> <div style="max-width:50%">${pretty(k)}</div> <div class="igpa-small">${w.toFixed(3)} ${STATE.locks[k]?'<span class="igpa-chip">locked</span>':''}</div> <div style="display:flex;gap:6px;flex-wrap:wrap"> <button class="igpa-btn" data-d="+">+0.05</button> <button class="igpa-btn" data-d="-">-0.05</button> <button class="igpa-btn" data-d="0">Zero</button> <button class="igpa-btn" data-lock="1">${STATE.locks[k]?'Unlock':'Lock'}</button> </div> </div> `).join('') || '<p class="igpa-small">Train the model to populate weights.</p>'; list.querySelectorAll('.igpa-row .igpa-btn').forEach(btn=>{ btn.onclick=()=>{ const row=btn.closest('.igpa-row'); const key=row.getAttribute('data-key'); if (btn.hasAttribute('data-lock')){ STATE.locks[key]=!STATE.locks[key]; persist(); render(); return; } const d=btn.getAttribute('data-d'); if (d==='+') STATE.weights[key]=(STATE.weights[key]||0)+0.05; else if (d==='-') STATE.weights[key]=(STATE.weights[key]||0)-0.05; else STATE.weights[key]=0; persist(); render(); }; }); }; render(); q.oninput=render; bodyEl.querySelector('#wnorm').onclick=()=>{ normalize(); render(); tip('Normalized weights'); }; bodyEl.querySelector('#wshrink').onclick=()=>{ l1shrink(); render(); tip('Applied L1 shrink'); }; } else if (name==='prefs'){ bodyEl.innerHTML = ` <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;"> <input id="pkey" placeholder="Add @creator, #hashtag, or keyword" style="flex:1;padding:8px;border-radius:10px;border:1px solid var(--igpa-border);background:var(--igpa-elev);color:var(--igpa-text)"/> <select id="ptype" style="padding:8px;border-radius:10px;border:1px solid var(--igpa-border);background:var(--igpa-elev);color:var(--igpa-text)"> <option value="pin">Pin/Boost</option><option value="ban">Ban/Mute</option><option value="snooze">Snooze (24h)</option> </select> <button class="igpa-btn igpa-btn--primary" id="padd">Add</button> </div> <h4>Pinned</h4><div id="ppins"></div> <h4>Banned</h4><div id="pbans"></div> <h4>Snoozed</h4><div id="psnz"></div> `; const render=()=>{ const pins = [ ...Object.keys(STATE.pins.creators).map(u=>({t:'@'+u, remove:()=>delete STATE.pins.creators[u]})), ...Object.keys(STATE.pins.hashtags).map(h=>({t:'#'+h, remove:()=>delete STATE.pins.hashtags[h]})), ...Object.keys(STATE.pins.keywords).map(k=>({t:k, remove:()=>delete STATE.pins.keywords[k]})) ]; const bans = [ ...Object.keys(STATE.bans.creators).map(u=>({t:'@'+u, remove:()=>delete STATE.bans.creators[u]})), ...Object.keys(STATE.bans.hashtags).map(h=>({t:'#'+h, remove:()=>{ delete STATE.bans.hashtags[h]; delete STATE.mutedHashtags[h]; }})), ...Object.keys(STATE.bans.keywords).map(k=>({t:k, remove:()=>delete STATE.bans.keywords[k]})) ]; const snz = Object.entries(STATE.snooze.creators).map(([u,ts])=>({t:`@${u} — ${new Date(ts).toLocaleString()}`, remove:()=>{ delete STATE.snooze.creators[u]; }})); fillList('#ppins', pins); fillList('#pbans', bans); fillList('#psnz', snz); function fillList(sel, items){ const box=bodyEl.querySelector(sel); box.innerHTML = items.length ? items.map(it=>{ const chipEl=document.createElement('span'); chipEl.className='igpa-chip'; chipEl.textContent=it.t+' '; const del=mkBtn('', 'igpa-btn--danger', {iconEl:icon('M6 6l12 12M18 6L6 18')}); del.title='Remove'; del.style.padding='4px 6px'; del.onclick=()=>{ it.remove(); persist(); render(); }; chipEl.appendChild(del); return chipEl.outerHTML; }).join('') : '<p class="igpa-small">Empty.</p>'; } }; bodyEl.querySelector('#padd').onclick=()=>{ const raw=bodyEl.querySelector('#pkey').value.trim(); const kind=bodyEl.querySelector('#ptype').value; if(!raw) return; if (raw.startsWith('@')){ const u=raw.slice(1).toLowerCase(); if (kind==='pin') STATE.pins.creators[u]=true; else if (kind==='ban') STATE.bans.creators[u]=true; else STATE.snooze.creators[u]=Date.now()+24*3600*1000; } else if (raw.startsWith('#')){ const h=raw.slice(1).toLowerCase(); if (kind==='pin') STATE.pins.hashtags[h]=true; else { STATE.bans.hashtags[h]=true; STATE.mutedHashtags[h]=true; } } else { const k=raw.toLowerCase(); if (kind==='pin') STATE.pins.keywords[k]=true; else STATE.bans.keywords[k]=true; } bodyEl.querySelector('#pkey').value=''; persist(); render(); }; render(); } else if (name==='settings'){ bodyEl.innerHTML = ` <div class="igpa-row"><div>Theme</div><div> <select id="theme" style="padding:8px;border-radius:10px;border:1px solid var(--igpa-border);background:var(--igpa-elev);color:var(--igpa-text)"> <option value="auto">Auto</option><option value="dark">Dark</option><option value="light">Light</option> </select></div> </div> <div class="igpa-row"><div>Accent</div><div><input id="accent" type="color" value="${STATE.themeAccent||CONFIG.ui.accent}" style="width:48px;height:32px;border:1px solid var(--igpa-border);border-radius:8px;background:var(--igpa-elev)"/></div></div> ${slider('LR A','lrA',CONFIG.learn.lrA,0.02,0.35,0.01)} ${slider('LR B','lrB',CONFIG.learn.lrB,0.02,0.35,0.01)} ${slider('Low threshold','lowThr',STATE.ui.lowThr,0.05,0.85,0.05)} ${slider('High threshold','highThr',STATE.ui.highThr,0.5,0.98,0.01)} ${slider('Max watch impact (s)','maxWatchImpactSec',CONFIG.learn.maxWatchImpactSec,10,120,5)} ${slider('Image weight','wImage',CONFIG.learn.wImage,0.4,1.8,0.05)} ${slider('Video weight','wVideo',CONFIG.learn.wVideo,0.4,2.0,0.05)} ${toggle('Show badge','showBadge',STATE.ui.showBadge)} ${toggle('Compact badge','compactBadge',STATE.ui.compactBadge)} ${toggle('Explain top feats','showExplain',STATE.ui.showExplain)} ${toggle('Dim low-score','dimLow',STATE.ui.dimLow)} ${toggle('Blur low-score','blurLow',STATE.ui.blurLow)} ${toggle('Highlight high-score','highlight',true)} ${toggle('Eye-comfort blur','eyeComfort',STATE.ui.eyeComfort)} ${toggle('Cap creator influence','capCreator',STATE.ui.capCreator)} ${toggle('Pause learning','pauseLearning',STATE.ui.pauseLearning)} ${toggle('Show HUD','showHUD',STATE.ui.showHUD)} ${toggle('Auto-skip low','autoSkip',STATE.ui.autoSkip)} <div class="igpa-row"><div>Auto-skip delay (ms)</div><input class="igpa-range" data-key="autoSkipDelay" type="range" min="250" max="2500" step="50" value="${STATE.ui.autoSkipDelay}"><div class="igpa-small">${STATE.ui.autoSkipDelay}</div></div> <div class="igpa-row"><div>Auto-skip jitter (ms)</div><input class="igpa-range" data-key="autoSkipJitter" type="range" min="0" max="1200" step="20" value="${STATE.ui.autoSkipJitter}"><div class="igpa-small">${STATE.ui.autoSkipJitter}</div></div> <hr/> <div style="display:flex; gap:8px; flex-wrap:wrap;"> <button class="igpa-btn" id="hide-low">Hide low in view</button> <button class="igpa-btn" id="show-all">Show all</button> <button class="igpa-btn igpa-btn--primary" id="jump-next">Jump High (J)</button> <button class="igpa-btn" id="selftest">Run Self-Test</button> </div> <pre id="selfout" class="igpa-small" style="margin-top:8px;white-space:pre-wrap;background:var(--igpa-elev);padding:8px;border-radius:10px;border:1px solid var(--igpa-border)"></pre> `; // Controls const theme=bodyEl.querySelector('#theme'); theme.value=STATE.ui.theme; theme.onchange=()=>{ STATE.ui.theme=theme.value; persist(); applyTheme(); }; const accent=bodyEl.querySelector('#accent'); accent.oninput=()=>{ STATE.themeAccent=accent.value; persist(); applyTheme(); }; // Ranges bodyEl.querySelectorAll('.igpa-range').forEach(r=>{ r.addEventListener('input',()=>{ const key=r.getAttribute('data-key'), val=Number(r.value); const lab=r.closest('.igpa-row')?.querySelector('.igpa-small'); if (lab) lab.textContent=(/skip|Jitter/i.test(key))?val:val.toFixed(2); if (key==='lrA') CONFIG.learn.lrA=val; else if (key==='lrB') CONFIG.learn.lrB=val; else if (key==='lowThr'){ STATE.ui.lowThr=val; STATE.adapt.baseLow=val; persist(); refreshAll(); } else if (key==='highThr'){ STATE.ui.highThr=val; persist(); refreshAll(); } else if (key==='autoSkipDelay'){ STATE.ui.autoSkipDelay=val; STATE.ui.autoSkip=true; persist(); } else if (key==='autoSkipJitter'){ STATE.ui.autoSkipJitter=val; persist(); } else if (key==='maxWatchImpactSec'){ CONFIG.learn.maxWatchImpactSec=val; persist(); } else if (key==='wImage'){ CONFIG.learn.wImage=val; persist(); } else if (key==='wVideo'){ CONFIG.learn.wVideo=val; persist(); } }); }); // Toggles bodyEl.querySelectorAll('input[type="checkbox"][data-key]').forEach(c=>{ c.addEventListener('change',()=>{ const k=c.getAttribute('data-key'); STATE.ui[k]=!!c.checked; persist(); if (k==='showHUD'){ if (STATE.ui.showHUD) buildHUD(); else removeHUD(); } refreshAll(); }); }); // Actions bodyEl.querySelector('#hide-low').onclick=()=>filterInView(true); bodyEl.querySelector('#show-all').onclick=()=>filterInView(false,true); bodyEl.querySelector('#jump-next').onclick=()=>jumpHigh('next'); bodyEl.querySelector('#selftest').onclick=async()=>{ const out=bodyEl.querySelector('#selfout'); out.textContent='Running...'; const res=await selfTest(); out.textContent = res.map(r=>`${r.ok?'✅':'❌'} ${r.name} — ${r.msg}`).join('\n'); }; } else if (name==='data'){ bodyEl.innerHTML = ` <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px;"> <button class="igpa-btn igpa-btn--primary" id="ex">Export JSON</button> <input type="file" id="im" accept="application/json" style="padding:8px;border-radius:10px;border:1px solid var(--igpa-border);background:var(--igpa-elev);color:var(--igpa-text)"/> <button class="igpa-btn" id="csv">Export CSV (history)</button> <button class="igpa-btn igpa-btn--danger" id="reset">Reset</button> </div> <h4>Snapshots</h4> <div style="display:flex; gap:8px; margin:6px 0;"> <input id="sname" placeholder="snapshot name" style="flex:1;padding:8px;border-radius:10px;border:1px solid var(--igpa-border);background:var(--igpa-elev);color:var(--igpa-text)"/> <button class="igpa-btn" id="ssave">Save</button> </div> <div id="slist"></div> `; bodyEl.querySelector('#ex').onclick=exportJSON; bodyEl.querySelector('#im').onchange=e=>importJSON(e.target.files[0]); bodyEl.querySelector('#csv').onclick=exportCSV; bodyEl.querySelector('#reset').onclick=resetAll; bodyEl.querySelector('#ssave').onclick=saveSnapshot; renderSnapshots(); } else if (name==='help'){ bodyEl.innerHTML = ` <p>This tool learns locally from your on-screen time and interactions. Labels are continuous (0..1) from watch/exposure, boosted by like/save. Nothing is sent anywhere.</p> <ul> <li><strong>Shortcuts:</strong> Ctrl+Shift+I (panel), J/K (jump next/prev high-score), H (toggle auto-skip).</li> <li><strong>Training:</strong> <${CONFIG.learn.minNegWatchSec}s → negative; ≥${CONFIG.learn.minPosWatchSec}s → positive; like/save adds +${CONFIG.learn.clickBoost} to the label.</li> <li><strong>Prefs:</strong> Pin/ban hashtags, keywords, creators; snooze creators 24h.</li> <li><strong>Data:</strong> Export/Import JSON, export history as CSV, snapshots.</li> <li><strong>Tuning:</strong> Weights tools under “Weights”; learning rates & thresholds under “Settings”.</li> </ul> `; } } const slider = (label,key,val,min,max,step)=>`<div class="igpa-row"><div>${label}</div><input class="igpa-range" data-key="${key}" type="range" min="${min}" max="${max}" step="${step}" value="${val}"><div class="igpa-small">${typeof val==='number'?val.toFixed(2):String(val)}</div></div>`; const toggle = (label,key,val)=>`<div class="igpa-row"><div>${label}</div><input type="checkbox" data-key="${key}" ${val?'checked':''}></div>`; const chip = (text, w)=>`<span class="igpa-chip">${text} <span class="igpa-small">${(w||0).toFixed(2)}</span></span>`; /*********************** * 11) BADGE & FOOTER * ***********************/ function paintBadge(el){ try{ const s = predict(el); if (!STATE.ui.showBadge){ el.querySelector(':scope > .igpa-badge')?.remove(); return; } let badge = el.querySelector(':scope > .igpa-badge'); if (!badge){ badge = document.createElement('div'); badge.className='igpa-badge'; if (!el.style.position || el.style.position==='static') el.style.position='relative'; el.appendChild(badge); } badge.classList.toggle('compact', !!STATE.ui.compactBadge); const pct = Math.round(s*100); const explain = STATE.ui.showExplain ? topExplain(el) : ''; const color = s>=STATE.ui.highThr ? 'var(--igpa-accent)' : s<effectiveLow() ? '#aaa' : '#ddd'; const creator = getUsername(el)||'unknown'; const cap = getCaptionText(el); const tags = extractHashtags(cap).slice(0,2); const kws = tokenize(cap).slice(0,2); const actions = [ `<button class="mini" data-act="pin">@${creator} ⊕</button>`, `<button class="mini" data-act="ban">@${creator} ⊖</button>`, `<button class="mini" data-act="snooze">@${creator} 💤</button>` ].concat( tags.flatMap(t=>[`<button class="mini" data-act="ptag" data-tag="${t}">#${t} ⊕</button>`,`<button class="mini" data-act="btag" data-tag="${t}">#${t} ⊖</button>`]), kws.flatMap(k=>[`<button class="mini" data-act="pkw" data-kw="${k}">${k} ⊕</button>`,`<button class="mini" data-act="bkw" data-kw="${k}">${k} ⊖</button>`]) ).join(''); badge.innerHTML = STATE.ui.compactBadge ? `<strong style="color:${color}">${pct}</strong>` : `<strong style="color:${color}">${pct}</strong> <span class="igpa-small">% match</span>${explain?`<div class="igpa-small" style="opacity:.85;margin-top:2px">${explain}</div>`:''} <div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:4px">${actions}</div>`; badge.querySelectorAll('.mini').forEach(btn=>{ btn.onclick=(ev)=>{ const act=btn.getAttribute('data-act'); if (act==='pin'){ STATE.pins.creators[creator]=true; } else if (act==='ban'){ STATE.bans.creators[creator]=true; } else if (act==='snooze'){ STATE.snooze.creators[creator]=Date.now()+24*3600*1000; } else if (act==='ptag'){ STATE.pins.hashtags[btn.getAttribute('data-tag')] = true; } else if (act==='btag'){ const t=btn.getAttribute('data-tag'); STATE.bans.hashtags[t]=true; STATE.mutedHashtags[t]=true; } else if (act==='pkw'){ STATE.pins.keywords[btn.getAttribute('data-kw')] = true; } else if (act==='bkw'){ STATE.bans.keywords[btn.getAttribute('data-kw')] = true; } persist(); paintBadge(el); ev.stopPropagation(); }; }); // Visual filters el.classList.remove('igpa-muted','igpa-blur','igpa-highlight'); if (STATE.ui.dimLow && s<effectiveLow()) el.classList.add('igpa-muted'); if ((STATE.ui.blurLow && s<effectiveLow()) || (STATE.ui.eyeComfort && (likelySponsored(el) || extractHashtags(cap).some(t=>STATE.mutedHashtags[t])))) el.classList.add('igpa-blur'); if (s>=STATE.ui.highThr) el.classList.add('igpa-highlight'); ensureFooter(el); } catch(e){ STATE.status.lastError='badge:'+String(e); } } function ensureFooter(el){ if (el.querySelector(':scope > .igpa-foot')) return; const foot=document.createElement('div'); foot.className='igpa-foot'; const up = document.createElement('button'); up.className='igpa-qbtn'; up.title='Thumb up'; up.textContent='👍'; const down= document.createElement('button'); down.className='igpa-qbtn'; down.title='Thumb down'; down.textContent='👎'; const hide= document.createElement('button'); hide.className='igpa-qbtn'; hide.title='Hide post'; hide.textContent='🫥'; foot.append(up, down, hide); el.appendChild(foot); up.onclick = ()=>manualTrain(el, 1, 'thumb'); down.onclick= ()=>manualTrain(el, 0, 'thumb'); hide.onclick= ()=>{ el.style.display='none'; }; } function manualTrain(el, label, via){ const id = getPostIdFromEl(el); let sec = 0; const rec=activeTimers.get(id); if (rec?.start){ sec += (now()-rec.start)/1000; rec.start=null; } el.querySelectorAll(SEL.video).forEach(v=>{ const tr=videoTrackers.get(v); if (!tr) return; if (tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.playing=false; } sec += tr.playAccum; tr.playAccum=0; }); const l = clamp01(Math.max(label, normWatch(sec, getPostType(el)))); const feats = featuresFromPost(el,{ watchBucket:watchBucket(sec) }); const upd = sgd(feats, l); const creator = getUsername(el)||'unknown'; recordEvent({ ts:Date.now(), id, creator, label:l, seconds:sec||null, via, pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed, feats:pickTop(feats) }); paintBadge(el); } function topExplain(el){ try{ const feats = featuresFromPost(el); const arr = Object.entries(feats).map(([k,v])=>[k,(STATE.weights[k]||0)*v]).sort((a,b)=>Math.abs(b[1])-Math.abs(a[1])).slice(0,2); return arr.map(([k,w])=>{ if (k.startsWith('tag:')) return `#${k.slice(4)}:${w.toFixed(2)}`; if (k.startsWith('user:')) return `@${k.slice(5)}:${w.toFixed(2)}`; if (k.startsWith('kw:')) return `${k.slice(3)}:${w.toFixed(2)}`; return `${k}:${w.toFixed(2)}`; }).join(' · '); }catch{return'';} } /******************* * 12) OBSERVERS * *******************/ let mo = null; function ensureMO(){ if (mo) return mo; mo = new MutationObserver(muts=>{ try{ const added = []; for (const m of muts){ m.addedNodes.forEach(node=>{ if (node.nodeType !== 1) return; const el = /** @type {HTMLElement} */(node); if (isPost(el)) added.push(el); el.querySelectorAll?.(SEL.article).forEach(p=>{ if (isPost(p)) added.push(p); }); }); } const set = new Set(); for (const el of added){ if (set.has(el)) continue; set.add(el); preparePost(el); } } catch (e){ STATE.status.lastError='mo:'+String(e); } }); return mo; } function preparePost(el){ try{ if (el.dataset?.igpaReady==='1') return; el.dataset.igpaReady='1'; ensureIO().observe(el); attachVideoTracker(el, getPostIdFromEl(el)); paintBadge(el); STATE.status.observing=true; } catch (e){ STATE.status.lastError='prep:'+String(e); } } /******************** * 13) NAV / FILTER * ********************/ function jumpHigh(dir='next'){ try{ const posts = [...document.querySelectorAll(SEL.article)].filter(isPost); const y = window.scrollY; const cand = posts.map(el=>({ el, s: predict(el), top: el.getBoundingClientRect().top + window.scrollY })) .filter(p=>p.s >= STATE.ui.highThr).sort((a,b)=>a.top-b.top); if (!cand.length){ tip('No high-score posts visible.'); return; } if (dir==='next'){ const c = cand.find(p=>p.top > y + 60) || cand[0]; window.scrollTo({ top: c.top-80, behavior:'smooth' }); } else { const c = [...cand].reverse().find(p=>p.top < y - 60) || cand[cand.length-1]; window.scrollTo({ top: c.top-80, behavior:'smooth' }); } } catch {} } function filterInView(hide=false, showAll=false){ document.querySelectorAll(SEL.article).forEach(el=>{ if(!isPost(el)) return; if (showAll){ el.style.display=''; paintBadge(el); return; } const s = predict(el); if (hide && s<effectiveLow()) el.style.display='none'; else el.style.display=''; paintBadge(el); }); } function refreshAll(){ document.querySelectorAll(SEL.article).forEach(el=>{ if (isPost(el)) paintBadge(el); }); updateHUD(); } /******************** * 14) DATA IO * ********************/ function recordEvent(ev){ STATE.history.push(ev); if (STATE.history.length>CONFIG.learn.maxEvents) STATE.history.shift(); if (ev.label>=0.5) STATE.stats.positives++; else STATE.stats.negatives++; STATE.stats.updatedAt = Date.now(); persist(); updateHUD(); } function prefix(weights, pref){ const out={}; for (const k in weights) if (k.startsWith(pref)) out[k]=weights[k]; return out; } function topN(obj, n){ return Object.entries(obj).filter(([k])=>k!=='__bias').sort((a,b)=>Math.abs(b[1])-Math.abs(a[1])).slice(0, n); } function pretty(k){ if (k.startsWith('tag:')) return '#'+k.slice(4); if (k.startsWith('user:')) return '@'+k.slice(5); if (k.startsWith('kw:')) return k.slice(3); return k; } function pickTop(feats){ try{ const arr = Object.entries(feats).map(([k,v])=>({k,v,w:STATE.weights[k]||0,contrib:(STATE.weights[k]||0)*v})); arr.sort((a,b)=>Math.abs(b.contrib)-Math.abs(a.contrib)); return arr.slice(0,6); } catch { return []; } } function exportJSON(){ const out = { version: CONFIG.version, schema: STATE.schema, themeAccent: STATE.themeAccent, state: STATE }; const blob = new Blob([JSON.stringify(out,null,2)], {type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='igpa-ultra-stable.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(url), 3000); } async function importJSON(file){ if (!file) return; try{ const txt = await file.text(); const obj = JSON.parse(txt); const st = obj.state || obj; if (!st || !st.weights || !st.ui) throw new Error('Invalid JSON'); STATE = migrate(Object.assign({}, DEFAULT_STATE, st)); persist(); refreshAll(); if (STATE.ui.showHUD) buildHUD(); else removeHUD(); tip('Imported settings.'); } catch { tip('Import failed.'); } } function exportCSV(){ try{ const rows=[['ts','id','creator','via','label','seconds','pred','loss','arm','lr']]; for (const ev of STATE.history){ rows.push([new Date(ev.ts).toISOString(), ev.id||'', ev.creator||'', ev.via||'', ev.label??'', ev.seconds??'', ev.pred??'', ev.loss??'', ev.arm||'', ev.lr??'']); } const csv=rows.map(r=>r.map(x=>String(x).replace(/"/g,'""')).map(x=>`"${x}"`).join(',')).join('\n'); const blob=new Blob([csv],{type:'text/csv'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='igpa-history.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(url), 3000); } catch { tip('CSV export failed.'); } } function saveSnapshot(){ const name=bodyEl?.querySelector('#sname')?.value?.trim(); if (!name){ tip('Name your snapshot'); return; } STATE.snapshots[name] = { ts: Date.now(), weights: STATE.weights, ui: STATE.ui, pins: STATE.pins, bans: STATE.bans, mutedHashtags: STATE.mutedHashtags, locks: STATE.locks }; persist(); renderSnapshots(); tip('Snapshot saved.'); } function renderSnapshots(){ const box=bodyEl?.querySelector('#slist'); if (!box) return; const items=Object.entries(STATE.snapshots).sort((a,b)=>b[1].ts-a[1].ts); box.innerHTML = items.length ? items.map(([n,s])=>` <div class="igpa-row"> <div>${n} <span class="igpa-small">(${new Date(s.ts).toLocaleString()})</span></div> <div> <button class="igpa-btn" data-s="${n}" data-act="load">Load</button> <button class="igpa-btn igpa-btn--danger" data-s="${n}" data-act="del">Delete</button> </div> </div> `).join('') : '<p class="igpa-small">No snapshots yet.</p>'; box.querySelectorAll('[data-s]').forEach(b=>{ b.onclick=()=>{ const n=b.getAttribute('data-s'); const act=b.getAttribute('data-act'); if (act==='load'){ const s=STATE.snapshots[n]; if (!s) return; STATE.weights=JSON.parse(JSON.stringify(s.weights)); STATE.ui=JSON.parse(JSON.stringify(s.ui)); STATE.pins=JSON.parse(JSON.stringify(s.pins)); STATE.bans=JSON.parse(JSON.stringify(s.bans)); STATE.mutedHashtags=JSON.parse(JSON.stringify(s.mutedHashtags)); STATE.locks=JSON.parse(JSON.stringify(s.locks||{})); persist(); refreshAll(); if (STATE.ui.showHUD) buildHUD(); else removeHUD(); tip('Snapshot loaded.'); } else { delete STATE.snapshots[n]; persist(); renderSnapshots(); } }; }); } function resetAll(){ if (!confirm('Reset model, preferences, and history?')) return; STATE = JSON.parse(JSON.stringify(DEFAULT_STATE)); persist(); refreshAll(); removeHUD(); tip('Reset complete.'); } /********************* * 15) SPARKLINES * *********************/ function drawSparkline(sel, arr, maxY=1){ const c = bodyEl.querySelector(sel); if (!c) return; const ctx = c.getContext('2d'); const W=c.width=c.clientWidth|0; const H=c.height=c.clientHeight|0; ctx.clearRect(0,0,W,H); ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--igpa-accent') || '#58c7fa'; ctx.lineWidth = 1.5; ctx.beginPath(); if (arr.length===0){ ctx.moveTo(2,H-2); ctx.lineTo(W-2,H-2); ctx.stroke(); return; } for (let i=0;i<arr.length;i++){ const x = (i/(arr.length-1||1))*(W-6)+3; const y = H - 3 - (clamp01((arr[i]||0)/maxY)*(H-6)); if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); } ctx.stroke(); } /********************* * 16) UTIL WEIGHTS * *********************/ function normalize(maxAbs=3.5){ const w=STATE.weights; for (const k in w){ if (k==='__bias') continue; w[k]=clamp(w[k], -maxAbs, maxAbs); } persist(); } function l1shrink(lambda=0.02){ const w=STATE.weights; for (const k in w){ if (k==='__bias') continue; w[k]*=(1-lambda); } persist(); } /********************* * 17) SELF-TEST * *********************/ async function selfTest(){ const results=[]; const ok = (name,msg='OK')=>results.push({ok:true,name,msg}); const bad= (name,msg)=>results.push({ok:false,name,msg}); try { typeof MutationObserver!=='undefined' ? ok('MutationObserver') : bad('MutationObserver','Missing'); } catch { bad('MutationObserver','Error'); } try { typeof IntersectionObserver!=='undefined' ? ok('IntersectionObserver') : bad('IntersectionObserver','Missing'); } catch { bad('IntersectionObserver','Error'); } try { document.createElement('canvas').getContext('2d') ? ok('Canvas') : bad('Canvas','2D context failed'); } catch { bad('Canvas','Error'); } try { const art = document.querySelector(SEL.article); art ? ok('Initial post detect') : bad('Initial post detect','No article node yet'); } catch { bad('Initial post detect','Error'); } try { const posts = [...document.querySelectorAll(SEL.article)].filter(isPost); posts.length ? ok('isPost heuristic',`${posts.length} candidates`) : bad('isPost heuristic','0 candidates'); } catch { bad('isPost heuristic','Error'); } await sleep(10); return results; } /***************** * 18) TIPS * *****************/ function tip(msg){ const t=document.createElement('div'); t.textContent=msg; Object.assign(t.style,{position:'fixed',left:'50%',bottom:'24px',transform:'translateX(-50%)',background:'var(--igpa-elev)',color:'var(--igpa-text)',padding:'8px 12px',borderRadius:'10px',border:'1px solid var(--igpa-border)',zIndex:String(CONFIG.ui.zIndex),boxShadow:'0 6px 18px rgba(0,0,0,.25)'}); document.body.appendChild(t); setTimeout(()=>t.remove(), 1400); } /***************** * 19) BOOT * *****************/ async function boot(){ try{ applyTheme(); buildHUD(); buildPanel(); // Wait for Instagram to paint something let tries=0; while (tries<200){ const ready = document.querySelector(SEL.postAnchor) || document.querySelector(SEL.article); if (ready) break; await sleep(80); tries++; } ensureMO().observe(document.body, { childList:true, subtree:true }); document.querySelectorAll(SEL.article).forEach(el=>{ if (isPost(el)) preparePost(el); }); setInterval(()=>{ STATE.status.observing = !!io; }, 1500); // Menu commands if (typeof GM_registerMenuCommand === 'function'){ GM_registerMenuCommand('Open Panel', ()=>panel?.classList.add('show')); GM_registerMenuCommand('Export JSON', exportJSON); GM_registerMenuCommand('Run Self-Test', async()=>{ const res=await selfTest(); alert(res.map(r=>`${r.ok?'✅':'❌'} ${r.name} — ${r.msg}`).join('\n')); }); } STATE.status.booted=true; persist(); log('Ultra Stable initialized'); } catch (e) { STATE.status.lastError = 'boot:'+String(e); persist(); warn('Boot error', e); } } // Start boot(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址