您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Local-only preference learner for Instagram with robust watch-time, AB-learning, rich overlays, filters, snapshots, resizable/responsive UI, per-media tuning, debug tools, creator overexposure dampener, and a modern button system (ripple, tooltips, segmented/toggles, icon buttons, quick actions). Fully standalone and polished.
// ==UserScript== // @name Instagram Preference AI + Insights // @namespace https://greasyfork.org/en/scripts/548510-instagram-preference-ai-insights-pro-pack-ultra // @version 1.0.0 // @description Local-only preference learner for Instagram with robust watch-time, AB-learning, rich overlays, filters, snapshots, resizable/responsive UI, per-media tuning, debug tools, creator overexposure dampener, and a modern button system (ripple, tooltips, segmented/toggles, icon buttons, quick actions). Fully standalone and polished. // @author You // @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'; /* ========================= Core: storage + logging ==========================*/ let DEBUG = false; // toggled in Settings const log = (...a)=>{ if (DEBUG) console.debug('[IG-PA Luxe]',...a); }; const warn = (...a)=>console.warn('[IG-PA Luxe]',...a); const gmGet=(k,def)=>{ try{ if(typeof GM_getValue==='function') return GM_getValue(k,def);}catch{} try{const r=localStorage.getItem('IGPALUXE_'+k);return r?JSON.parse(r):def;}catch{return def;} }; const gmSet=(k,v)=>{ try{ if(typeof GM_setValue==='function') return GM_setValue(k,v);}catch{} try{ localStorage.setItem('IGPALUXE_'+k,JSON.stringify(v)); }catch{} }; const 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{} }; /* ========================= Config ==========================*/ const CONFIG = { version: '2.0.0', ui: { zIndex: 2147483000, fabSize: 56, defaultPanelWidth: Math.min(560, Math.max(380, Math.floor(window.innerWidth*0.44))), defaultPanelHeight: Math.min(720, Math.max(520, Math.floor(window.innerHeight*0.68))), minPanelWidth: 360, minPanelHeight: 420, badgeBorderRadius: 12 }, learn: { lr: 0.10, lrA: 0.10, lrB: 0.20, abPeriod: 90, minNegWatchSec: 1.0, minPosWatchSec: 6.0, maxWatchImpactSec: 30.0, watchWeightImage: 1.00, watchWeightVideo: 1.12, clickBoost: 0.16, decay: 0.0006, maxEvents: 6000, vocab: { maxHashtags: 220, maxCreators: 240, maxStems: 650 }, overexposureWindow: 10, overexposurePenalty: 0.09 }, uiPrefsDefault: { theme: 'auto', // auto|dark|light showBadge: true, compactBadge: false, showExplain: true, dimLowScore: true, blurLowScore: false, lowScoreThreshold: 0.40, highlightHighScore: true, highScoreThreshold: 0.78, eyeComfort: true, capCreator: true, creatorCapWeight: 1.18, autoSkipLow: false, autoSkipDelayMs: 760, autoskipJitterMax: 260, pauseLearning: false, showDebugOverlay: false } }; const SELECTORS = { 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'] }; /* ========================= Utils ==========================*/ const clamp01 = x=>Math.min(1,Math.max(0,x)); const EPS = 1e-7; const sigmoid = z => z>=0 ? 1/(1+Math.exp(-z)) : Math.exp(z)/(1+Math.exp(z)); const now = ()=>performance.now(); const sleep = ms=>new Promise(r=>setTimeout(r,ms)); const randInt = n => (Math.random()*n)|0; const 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(SELECTORS.postAnchor); if (a){ const url = new URL(a.href, location.origin); const parts = url.pathname.split('/').filter(Boolean); if (parts.length >= 2) return `${parts[0]}:${parts[1]}`; } const dataId = el.getAttribute('data-testid') || el.getAttribute('data-post-id'); if (dataId) return 'd:'+dataId; return 'x:'+hashStr((el.textContent||'').slice(0,420)); } catch { return 'x:'+Math.random().toString(36).slice(2); } } function isPost(el){ if (!(el instanceof HTMLElement)) return false; const a = el.querySelector(SELECTORS.postAnchor); const hasVid = !!el.querySelector(SELECTORS.video); const hasImg = !!el.querySelector('img'); const roleArticle = el.matches(SELECTORS.article); return !!(a || hasVid || (roleArticle && hasImg)); } function getUsername(el){ try{ const h = el.querySelector(SELECTORS.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 SELECTORS.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 < 3200) best = txt; } if (best) break; } } catch {} return best.trim(); } 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]; }; const getPostType = el=>{ try{ if (el.querySelector(SELECTORS.video)) return 'video'; const imgs = el.querySelectorAll('img'); if (imgs && imgs.length >= 2) return 'carousel'; return 'image'; } catch { return 'image'; } }; const getCaptionLenBucket = len => len<40?'cap:short':(len<140?'cap:med':'cap:long'); const SPONSORED_RE = /\b(sponsored|advert(?:isement|orial)?|paid (?:partnership|collab(?:oration)?)|promoted|partnered with)\b/i; const likelySponsored = el => { const txt = (el.innerText||'').toLowerCase(); return SPONSORED_RE.test(txt) || !!el.querySelector('[aria-label*="Paid partnership" i]'); }; 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(' ')); 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); } function stem(w){ return w.replace(/(ing|ers|er|ies|ied|iness|ness|ments|ment|ation|ations)$/,'') .replace(/(ed|ly|es|s)$/,'') .replace(/(.)\1{2,}/g,'$1'); } /* ========================= State ==========================*/ const DEFAULT_STATE = { ui: {...CONFIG.uiPrefsDefault}, weights: { __bias: 0 }, stats: { events: 0, positives: 0, negatives: 0, updatedAt: Date.now(), totalWatchSec:0, totalImageExposureSec:0, totalVideoPlaySec:0 }, pins: { creators: {}, hashtags: {}, keywords: {} }, bans: { creators: {}, hashtags: {}, keywords: {} }, mutedHashtags: {}, history: [], // {ts,id,label,seconds,feats,via,pred,loss,arm,lr,metaCreator} 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 } }; let STATE = Object.assign({}, DEFAULT_STATE, gmGet('state_luxe', DEFAULT_STATE)); function persist(){ gmSet('state_luxe', STATE); DEBUG = !!STATE.ui.showDebugOverlay; } /* ========================= Features & Score ==========================*/ function overexposureDampener(creator){ try{ const N = CONFIG.learn.overexposureWindow; const recent = STATE.history.slice(-N); let count = 0; for (const ev of recent){ if (ev?.metaCreator === creator) count++; } if (count <= Math.floor(N*0.4)) return 0; const ratio = count / Math.max(1,N); return Math.min(1, ratio) * CONFIG.learn.overexposurePenalty; } catch { return 0; } } function featuresFromPost(el, extra={}){ const feats = {}; const type = getPostType(el); feats['type:'+type] = 1; const creator = (getUsername(el)||'unknown'); feats['user:'+creator] = 1; const caption = getCaptionText(el); const tags = extractHashtags(caption).filter(t=>t.length<=32); for (const t of tags) feats['tag:'+t] = 1; for (const s of tokenize(caption).slice(0,40)) feats['kw:'+s] = 1; feats[getCaptionLenBucket(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; if (extra.watchBucket) feats['watch:'+extra.watchBucket] = 1; if (extra.exposureHeavy) feats['watch:exposureHeavy'] = 1; if (extra.playHeavy) feats['watch:playHeavy'] = 1; const damp = overexposureDampener(creator); if (damp > 0) feats['reg:overexposed'] = damp; 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), v = feats[k]; if (STATE.ui.capCreator && creatorKey && k===creatorKey){ const cap = STATE.ui.creatorCapWeight; 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; } z += w*v; } return z; } const predictScore = (el, feats=null)=> sigmoid(dot(STATE.weights, feats||featuresFromPost(el))); /* ========================= Learning + AB ==========================*/ 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 dropN = Math.min(keys.length - max, Math.max(1, Math.floor(keys.length*0.12))); for(let i=0;i<dropN;i++) delete w[keys[i]]; } catch(e){ STATE.status.lastError='prune:'+String(e); } } function sgdUpdate(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) 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 }; } const pickTopFeats = 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 []; } }; /* ========================= Watch-time: videos & images ==========================*/ const activeTimers = new Map(); // id -> { el, start, accum, autoskipTimer, skipped } const videoTrackers = new Map(); // video -> { id, playAccum, lastTick, playing, cleanup } function normalizedWatchLabel(sec, type='image'){ const L=CONFIG.learn, x=clamp01(sec/L.maxWatchImpactSec), smooth=x*x*(3-2*x); let label = sec<L.minNegWatchSec?0 : (sec>=L.minPosWatchSec?1:smooth); const mult = type==='video'?L.watchWeightVideo:L.watchWeightImage; return clamp01(label*mult); } const watchBucket = sec=> sec<2?'tiny':(sec<5?'short':(sec<10?'med':(sec<20?'long':'vlong'))); let io=null; function ensureIO(){ if (io) return io; io = new IntersectionObserver(entries=>{ try{ 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 }; rec.el=el; if (entry.isIntersecting && entry.intersectionRatio>=0.55){ if (!rec.start) rec.start=now(); scheduleAutoSkip(el, id, rec); } else { if (rec.start){ rec.accum += (now()-rec.start)/1000; rec.start=null; onExposureCompleteImageSide(el, id, rec.accum); rec.accum=0; } if (rec.autoskipTimer){ clearTimeout(rec.autoskipTimer); rec.autoskipTimer=null; } } activeTimers.set(id, rec); } }catch(e){ STATE.status.lastError='io:'+String(e); } },{ threshold:[0,0.55,1] }); return io; } function attachVideoTracker(el, id){ try{ el.querySelectorAll(SELECTORS.video).forEach(v=>{ if (videoTrackers.has(v)) return; const tr = { id, playAccum:0, lastTick:0, playing:false }; const onPlay = ()=>{ tr.playing=true; tr.lastTick=now(); v.setAttribute('data-igpa-playing','1'); }; const onPause= ()=>{ if(tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.playing=false; } v.removeAttribute('data-igpa-playing'); }; const onTime = ()=>{ if(tr.playing){ tr.playAccum+=(now()-tr.lastTick)/1000; tr.lastTick=now(); } }; const onEnd = ()=>{ onPause(); flushVideoTracker(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='vid:'+String(e); } } function flushVideoTracker(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 el = activeTimers.get(tr.id)?.el || document.querySelector(SELECTORS.article); if (el) onVideoPlayComplete(el, tr.id, tr.playAccum); tr.playAccum=0; }catch{} } function onExposureCompleteImageSide(el, id, seconds){ try{ if (getPostType(el)==='video' || seconds<0.01) return; STATE.stats.totalWatchSec += seconds; STATE.stats.totalImageExposureSec += seconds; const nlabel = normalizedWatchLabel(seconds,'image'); const label = seconds<CONFIG.learn.minNegWatchSec?0:nlabel; const extra = { watchBucket:watchBucket(seconds), exposureHeavy:1 }; const feats = featuresFromPost(el, extra); const upd = sgdUpdate(feats, label); const creator = getUsername(el)||'unknown'; addHistory({ ts:Date.now(), id, metaCreator:creator, label, seconds, feats:pickTopFeats(feats), via:'exposure', pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed }); persist(); paintBadge(el); }catch(e){ STATE.status.lastError='imgwatch:'+String(e); } } function onVideoPlayComplete(el, id, seconds){ try{ if (seconds<0.01) return; STATE.stats.totalWatchSec += seconds; STATE.stats.totalVideoPlaySec += seconds; const nlabel = normalizedWatchLabel(seconds,'video'); const label = seconds<CONFIG.learn.minNegWatchSec?0:nlabel; const extra = { watchBucket:watchBucket(seconds), playHeavy:1 }; const feats = featuresFromPost(el, extra); const upd = sgdUpdate(feats, label); const creator = getUsername(el)||'unknown'; addHistory({ ts:Date.now(), id, metaCreator:creator, label, seconds, feats:pickTopFeats(feats), via:'video', pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed }); persist(); paintBadge(el); }catch(e){ STATE.status.lastError='vidwatch:'+String(e); } } /* ========================= Likes / Saves (heuristics) ==========================*/ const 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;} }; const 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(SELECTORS.article); if (!el || !isPost(el)) return; 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 && rec.start){ sec+=(now()-rec.start)/1000; rec.start=null; } el.querySelectorAll(SELECTORS.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 nlabel = clamp01(Math.max(normalizedWatchLabel(sec,getPostType(el)),0) + CONFIG.learn.clickBoost); const feats = featuresFromPost(el, { watchBucket:watchBucket(sec) }); const upd = sgdUpdate(feats, nlabel); const creator=getUsername(el)||'unknown'; addHistory({ ts:Date.now(), id, metaCreator:creator, label:nlabel, seconds:sec||null, feats:pickTopFeats(feats), via:liked?'like':'save', pred:upd.pred, loss:upd.loss, arm:upd.arm, lr:upd.lrUsed }); persist(); paintBadge(el); },120); }catch(e2){ STATE.status.lastError='click:'+String(e2); } }, true); function flushActiveTimers(){ 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)) onExposureCompleteImageSide(rec.el,id,rec.accum); rec.accum=0; } rec.el?.querySelectorAll?.(SELECTORS.video).forEach(v=>{ const tr=videoTrackers.get(v); if (tr) flushVideoTracker(v,tr); }); }catch{} } } window.addEventListener('pagehide', flushActiveTimers); document.addEventListener('visibilitychange', ()=>{ if(document.visibilityState!=='visible') flushActiveTimers(); }); /* ========================= Auto-skip (optional) ==========================*/ function scheduleAutoSkip(el,id,rec){ try{ if (!STATE.ui.autoSkipLow || rec.autoskipTimer || rec.skipped) return; const delay = STATE.ui.autoSkipDelayMs + randInt(STATE.ui.autoskipJitterMax); rec.autoskipTimer=setTimeout(()=>{ try{ const score=predictScore(el); if (score<STATE.ui.lowScoreThreshold){ rec.skipped=true; const r=el.getBoundingClientRect(); window.scrollBy({ top: Math.max(140, r.height+32), behavior:'smooth' }); } } finally { rec.autoskipTimer=null; } }, delay); }catch{} } /* ========================= Aesthetic system: buttons, ripple, tooltips, segmented, icons ==========================*/ addStyle(` :root{--igpa-bg:#0b0b0b;--igpa-elev:#121212;--igpa-elev2:#1a1a1a;--igpa-text:#eee;--igpa-sub:#a9a9a9;--igpa-border:rgba(255,255,255,.08);--igpa-accent:#58c7fa;--igpa-success:#38b000;--igpa-danger:#ff5d5d;--igpa-warn:#ffb020} [data-igpa-theme="light"]{--igpa-bg:#f7f7f7;--igpa-elev:#ffffff;--igpa-elev2:#f0f0f0;--igpa-text:#0a0a0a;--igpa-sub:#565656;--igpa-border:rgba(0,0,0,.12);--igpa-accent:#007aff;--igpa-success:#119a00;--igpa-danger:#cc1a1a;--igpa-warn:#b97a00} /* FAB */ .igpa-fab{position:fixed;right:16px;bottom:16px;width:${CONFIG.ui.fabSize}px;height:${CONFIG.ui.fabSize}px;border-radius:16px;background:linear-gradient(180deg,var(--igpa-elev),var(--igpa-elev2));color:var(--igpa-text);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:${CONFIG.ui.zIndex};box-shadow:0 8px 24px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.05);user-select:none;border:1px solid var(--igpa-border);backdrop-filter:blur(4px)} .igpa-fab:hover{transform:translateY(-1px)} .igpa-fab svg{width:24px;height:24px} /* Panel */ .igpa-panel{position:fixed;right:96px;bottom:16px;background:var(--igpa-bg);color:var(--igpa-text);border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,.45);z-index:${CONFIG.ui.zIndex};display:none;overflow:hidden;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;border:1px solid var(--igpa-border)} .igpa-panel.show{display:flex;flex-direction:column} .igpa-titlebar{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--igpa-elev);cursor:move;border-bottom:1px solid var(--igpa-border)} .igpa-titlebar .title{display:flex;align-items:center;gap:8px} .igpa-toolbar{display:flex;gap:8px;align-items:center;padding:8px;background:var(--igpa-elev);border-bottom:1px solid var(--igpa-border);flex-wrap:wrap} .igpa-tabs{display:flex;flex-wrap:wrap;gap:8px;padding:8px 10px;border-bottom:1px solid var(--igpa-border);background:var(--igpa-elev)} .igpa-tab{padding:6px 10px;border-radius:10px;background:transparent;cursor:pointer;border:1px solid var(--igpa-border)} .igpa-tab.active{background:rgba(255,255,255,.06)} .igpa-body{flex:1;overflow:auto;padding:12px} .igpa-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 0} .igpa-range{width:55%} /* Buttons: variants + ripple */ .igpa-btn{position:relative;display:inline-flex;gap:8px;align-items:center;justify-content:center;padding:8px 12px;border-radius:10px;border:1px solid var(--igpa-border);background:linear-gradient(180deg,var(--igpa-elev),var(--igpa-elev2));color:var(--igpa-text);cursor:pointer;user-select:none;transition:transform .08s ease, box-shadow .15s ease} .igpa-btn:hover{transform:translateY(-1px)} .igpa-btn:active{transform:translateY(0)} .igpa-btn[aria-pressed="true"]{box-shadow:0 0 0 2px var(--igpa-accent) inset} .igpa-btn--primary{background:linear-gradient(180deg,rgba(88,199,250,.95),rgba(88,199,250,.85));color:#021014;border-color:rgba(2,16,20,.2)} .igpa-btn--danger{background:linear-gradient(180deg,rgba(255,93,93,.95),rgba(255,93,93,.85));color:#200;border-color:rgba(0,0,0,.15)} .igpa-btn--ghost{background:transparent} .igpa-btn--icon{padding:8px;width:36px;height:36px} .igpa-btn svg{width:16px;height:16px} .igpa-ripple{position:absolute;border-radius:999px;transform:scale(0);opacity:.55;pointer-events:none;background:currentColor;mix-blend-mode:overlay;animation:igpa-ripple .5s linear} @keyframes igpa-ripple{to{transform:scale(10);opacity:0}} /* Segmented */ .igpa-seg{display:inline-flex;border:1px solid var(--igpa-border);border-radius:12px;overflow:hidden} .igpa-seg .igpa-btn{border:0;border-radius:0;background:var(--igpa-elev);padding:8px 10px} .igpa-seg .igpa-btn.active{background:var(--igpa-elev2);box-shadow:inset 0 0 0 2px var(--igpa-accent)} /* Tooltip */ .igpa-tip{position:absolute;padding:6px 8px;background:var(--igpa-elev);border:1px solid var(--igpa-border);color:var(--igpa-text);border-radius:8px;font-size:12px;white-space:nowrap;z-index:${CONFIG.ui.zIndex};box-shadow:0 6px 20px rgba(0,0,0,.25)} .igpa-tip[data-hide="1"]{display:none} /* Badge */ .igpa-badge{position:absolute;top:8px;right:8px;padding:6px 10px;font-size:12px;line-height:18px;background:rgba(0,0,0,.62);color:#fff;border-radius:${CONFIG.ui.badgeBorderRadius}px;border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(3px);z-index:${CONFIG.ui.zIndex - 1};pointer-events:auto} [data-igpa-theme="light"] .igpa-badge{background:rgba(255,255,255,.82);color:#000;border:1px solid rgba(0,0,0,.12)} .igpa-badge.compact{padding:4px 8px;font-size:11px} .igpa-badge .pct{font-weight:700} .igpa-badge .exp{font-size:10px;opacity:.85;display:block;margin-top:2px} .igpa-badge .mini-btn{margin-left:6px;padding:0 6px;border-radius:6px;background:var(--igpa-elev);border:1px solid var(--igpa-border);cursor:pointer} /* Post effects */ .igpa-muted{filter:grayscale(.25) brightness(.8);opacity:.55} .igpa-blur{filter:blur(2px) grayscale(.2) brightness(.9)} .igpa-highlight{outline:2px solid var(--igpa-accent);outline-offset:-2px;box-shadow:0 0 0 2px rgba(88,199,250,.35) inset;border-radius:10px} /* Footer quick actions (fancy buttons) */ .igpa-foot{position:absolute;left:8px;bottom:8px;display:flex;gap:6px;z-index:${CONFIG.ui.zIndex - 1}} .igpa-qbtn{position:relative;display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;border:1px solid var(--igpa-border);background:linear-gradient(180deg,var(--igpa-elev),var(--igpa-elev2));color:var(--igpa-text);cursor:pointer} .igpa-qbtn:hover{transform:translateY(-1px)} .igpa-qbtn svg{width:16px;height:16px} .igpa-kv{display:grid;grid-template-columns:170px 1fr;gap:6px} .igpa-chip{display:inline-flex;gap:6px;align-items:center;padding:4px 8px;background:var(--igpa-elev);border:1px solid var(--igpa-border);border-radius:999px;margin:3px} .igpa-small{font-size:12px;opacity:.8} .igpa-session{display:grid;grid-template-columns:auto 1fr auto;gap:6px;align-items:center} .igpa-resize{position:absolute;right:6px;bottom:6px;width:14px;height:14px;border-right:2px solid var(--igpa-sub);border-bottom:2px solid var(--igpa-sub);cursor:nwse-resize;opacity:.7} @media (max-width: 920px){ .igpa-panel{right:12px;left:12px !important;width:auto !important} .igpa-range{width:52%} .igpa-kv{grid-template-columns:140px 1fr} } `); /* ========================= Theme, panel, and fancy buttons infra ==========================*/ const setThemeAttr=()=>{ let t = STATE.ui.theme; if (t==='auto'){ const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; t = prefersDark ? 'dark' : 'light'; } document.documentElement.setAttribute('data-igpa-theme', t==='dark'?'dark':'light'); }; function mkIcon(path,view='0 0 24 24'){ const s=document.createElementNS('http://www.w3.org/2000/svg','svg'); s.setAttribute('viewBox',view); const p=document.createElementNS('http://www.w3.org/2000/svg','path'); p.setAttribute('d',path); s.appendChild(p); return s; } function mkBtn(txt='', variant='', {icon=null, tooltip=null, pressed=null}={}){ const b=document.createElement('button'); b.className='igpa-btn'+(variant?(' '+variant):''); b.type='button'; if (icon){ b.appendChild(icon); if (txt) { const sp=document.createElement('span'); sp.textContent=txt; b.appendChild(sp); } } else b.textContent=txt; if (pressed!=null){ b.setAttribute('aria-pressed', String(!!pressed)); } attachRipple(b); if (tooltip) attachTooltip(b, tooltip); return b; } function attachRipple(el){ el.addEventListener('click',e=>{ const r=document.createElement('span'); r.className='igpa-ripple'; const rect=el.getBoundingClientRect(); const size=Math.max(rect.width, rect.height); r.style.width=r.style.height=size+'px'; r.style.left=(e.clientX-rect.left-size/2)+'px'; r.style.top=(e.clientY-rect.top-size/2)+'px'; el.appendChild(r); setTimeout(()=>r.remove(), 500); },{passive:true}); } function attachTooltip(el, text){ let tip; const show=(e)=>{ if (tip) return; tip=document.createElement('div'); tip.className='igpa-tip'; tip.textContent=text; document.body.appendChild(tip); const r=el.getBoundingClientRect(); const tr=tip.getBoundingClientRect(); const x=r.left + (r.width-tr.width)/2; const y=r.top - tr.height - 8; tip.style.left=Math.max(8, Math.min(window.innerWidth-tr.width-8, x))+'px'; tip.style.top=Math.max(8, y)+'px'; }; const hide=()=>{ if(tip){ tip.remove(); tip=null; } }; el.addEventListener('mouseenter',show); el.addEventListener('mouseleave',hide); el.addEventListener('blur',hide); } /* ========================= Panel + FAB + Toolbar (segmented/toggles) ==========================*/ let fab=null, panel=null, bodyEl=null; function buildUI(){ if (fab && panel) return; // FAB fab=document.createElement('div'); fab.className='igpa-fab'; fab.title='Preference AI & Insights'; fab.setAttribute('role','button'); fab.tabIndex=0; const star=mkIcon('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'); fab.appendChild(star); fab.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' ') fab.click(); }); document.documentElement.appendChild(fab); // Panel panel=document.createElement('div'); panel.className='igpa-panel'; const w=STATE.panel.w||CONFIG.ui.defaultPanelWidth, h=STATE.panel.h||CONFIG.ui.defaultPanelHeight; panel.style.width=w+'px'; panel.style.height=h+'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 titlebar=document.createElement('div'); titlebar.className='igpa-titlebar'; const title=document.createElement('div'); title.className='title'; title.innerHTML=`<strong>IG Preference AI — Luxe</strong> <span class="igpa-small">v${CONFIG.version}</span>`; const winBtns=document.createElement('div'); winBtns.style.display='flex'; winBtns.style.gap='8px'; const btnMin=mkBtn('', 'igpa-btn--icon', {icon:mkIcon('M6 12h12v2H6z'), tooltip:'Minimize / Restore'}); btnMin.dataset.act='minimize'; const btnNext=mkBtn('Next', 'igpa-btn--primary', {icon:mkIcon('M8 5l8 7-8 7', '0 0 24 24'), tooltip:'Next High-Score (J)'}); btnNext.dataset.act='next'; const btnClose=mkBtn('', 'igpa-btn--icon igpa-btn--danger', {icon:mkIcon('M6 6l12 12M18 6L6 18'), tooltip:'Close'}); btnClose.dataset.act='close'; winBtns.append(btnMin,btnNext,btnClose); titlebar.append(title,winBtns); // Toolbar (segmented filters + quick toggles) const toolbar=document.createElement('div'); toolbar.className='igpa-toolbar'; const seg=document.createElement('div'); seg.className='igpa-seg'; const segAll=mkBtn('All','',{pressed:true}); const segHi=mkBtn('High','',{}); const segLow=mkBtn('Low','',{}); segAll.classList.add('active'); segAll.dataset.seg='all'; segHi.dataset.seg='high'; segLow.dataset.seg='low'; seg.append(segAll,segHi,segLow); const tAuto=mkBtn('Auto-skip','', {icon:mkIcon('M12 5v4l3 3M4 12a8 8 0 1 0 16 0 8 8 0 0 0-16 0'), tooltip:'Toggle auto-skip', pressed:STATE.ui.autoSkipLow}); tAuto.dataset.toggle='autoskip'; const tPause=mkBtn('Pause','', {icon:mkIcon('M8 6h3v12H8zM13 6h3v12h-3z'), tooltip:'Pause learning', pressed:STATE.ui.pauseLearning}); tPause.dataset.toggle='pause'; const tDebug=mkBtn('Debug','', {icon:mkIcon('M12 8a4 4 0 100 8 4 4 0 000-8zm0-6v3m0 16v3m-9-9H0m24 0h-3M4.2 4.2L2 2m20 20-2.2-2.2'), tooltip:'Show debug overlay', pressed:STATE.ui.showDebugOverlay}); tDebug.dataset.toggle='debug'; toolbar.append(seg, tAuto, tPause, tDebug); // Tabs const tabs=document.createElement('div'); tabs.className='igpa-tabs'; const tabNames=['overview','session','weights','prefs','settings','data','help']; tabNames.forEach((n,i)=>{ const tb=mkBtn(n.charAt(0).toUpperCase()+n.slice(1),'',{}); tb.classList.add('igpa-tab'); if (i===0) tb.classList.add('active'); tb.dataset.tab=n; tabs.appendChild(tb); }); bodyEl=document.createElement('div'); bodyEl.className='igpa-body'; const resizer=document.createElement('div'); resizer.className='igpa-resize'; resizer.title='Resize'; panel.append(titlebar, toolbar, tabs, bodyEl, resizer); document.documentElement.appendChild(panel); // Drag let dragging=false,sx=0,sy=0,px=0,py=0; titlebar.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.right='auto'; panel.style.bottom='auto'; panel.style.left=(px+(e.clientX-sx))+'px'; panel.style.top=(py+(e.clientY-sy))+'px'; }); document.addEventListener('mouseup',()=>{ if(!dragging) return; dragging=false; persistPanelPos(); }); // Resize let resizing=false, rw=0, rh=0, rsx=0, rsy=0; resizer.addEventListener('mousedown',e=>{ resizing=true; rsx=e.clientX; rsy=e.clientY; const r=panel.getBoundingClientRect(); rw=r.width; rh=r.height; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove',e=>{ if(!resizing) return; let nw=Math.max(CONFIG.ui.minPanelWidth, rw+(e.clientX-rsx)); let 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(); }); // Actions function togglePanel(force){ const show=(force!==undefined)?force:!panel.classList.contains('show'); panel.classList.toggle('show',show); if (!show) STATE.panel.minimized=false; persist(); } fab.addEventListener('click',()=>togglePanel(true)); btnClose.addEventListener('click',()=>togglePanel(false)); btnNext.addEventListener('click',()=>scrollToNextHighScore()); btnMin.addEventListener('click',()=>{ STATE.panel.minimized=!STATE.panel.minimized; if (STATE.panel.minimized){ panel.style.height='52px'; bodyEl.style.display='none'; toolbar.style.display='none'; } else { panel.style.height=(STATE.panel.h||CONFIG.ui.defaultPanelHeight)+'px'; bodyEl.style.display='block'; toolbar.style.display='flex'; } persist(); }); // Toolbar behavior seg.addEventListener('click',e=>{ const b=e.target.closest('.igpa-btn'); if(!b) return; seg.querySelectorAll('.igpa-btn').forEach(x=>{ x.classList.remove('active'); x.removeAttribute('aria-pressed'); }); b.classList.add('active'); b.setAttribute('aria-pressed','true'); const kind=b.dataset.seg; if (kind==='all') applyVisualFilters(false,true); if (kind==='high'){ // show only high document.querySelectorAll(SELECTORS.article).forEach(el=>{ if(!isPost(el)) return; const s=predictScore(el); el.style.display = (s>=STATE.ui.highScoreThreshold)?'':'none'; paintBadge(el); }); } if (kind==='low'){ // show only low document.querySelectorAll(SELECTORS.article).forEach(el=>{ if(!isPost(el)) return; const s=predictScore(el); el.style.display = (s<STATE.ui.lowScoreThreshold)?'':'none'; paintBadge(el); }); } }); const toggle = (b,key)=>{ const on=!JSON.parse(b.getAttribute('aria-pressed')||'false'); b.setAttribute('aria-pressed',String(on)); if (key==='autoskip'){ STATE.ui.autoSkipLow=on; tip(`Auto-skip ${on?'ON':'OFF'}`); } if (key==='pause'){ STATE.ui.pauseLearning=on; tip(`Learning ${on?'PAUSED':'ACTIVE'}`); } if (key==='debug'){ STATE.ui.showDebugOverlay=on; DEBUG=on; } persist(); }; tAuto.addEventListener('click',()=>toggle(tAuto,'autoskip')); tPause.addEventListener('click',()=>toggle(tPause,'pause')); tDebug.addEventListener('click',()=>toggle(tDebug,'debug')); // Tabs render tabs.addEventListener('click',e=>{ const tb=e.target.closest('.igpa-tab'); if(!tb) return; tabs.querySelectorAll('.igpa-tab').forEach(t=>t.classList.remove('active')); tb.classList.add('active'); renderTab(tb.dataset.tab); }); // 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(); if(e.code==='KeyJ'){ jumpHighScore('next'); } if(e.code==='KeyK'){ jumpHighScore('prev'); } if(e.code==='KeyH'){ STATE.ui.autoSkipLow=!STATE.ui.autoSkipLow; persist(); tAuto.setAttribute('aria-pressed',String(STATE.ui.autoSkipLow)); tip(`Auto-skip ${STATE.ui.autoSkipLow?'ON':'OFF'}`); } }); // First render setThemeAttr(); renderTab('overview'); if (!panel.classList.contains('show')) panel.classList.add('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{} } /* ========================= Render Tabs ==========================*/ function renderTab(name){ if (!panel || !bodyEl) return; if (name==='overview'){ const topTags=topN(weightSubset('tag:'),12), topUsers=topN(weightSubset('user:'),12), topKWs=topN(weightSubset('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:#f88">(${STATE.status.lastError})</span>`:''}</div> <div>Events</div><div>${STATE.stats.events}</div> <div>Labels</div><div><span class="igpa-chip">≥0.5: ${STATE.stats.positives}</span> <span class="igpa-chip"><0.5: ${STATE.stats.negatives}</span></div> <div>Watch-time</div><div>${STATE.stats.totalWatchSec.toFixed(1)}s — <span class="igpa-small">vid ${STATE.stats.totalVideoPlaySec.toFixed(1)}s / img ${STATE.stats.totalImageExposureSec.toFixed(1)}s</span></div> <div>Active LR</div><div>${CONFIG.learn.lr.toFixed(2)} (A:${CONFIG.learn.lrA}, B:${CONFIG.learn.lrB})</div> <div>Theme</div><div><span class="igpa-pill">${STATE.ui.theme}</span></div> <div>Learning</div><div><span class="igpa-pill">${STATE.ui.pauseLearning?'Paused ⏸️':'Active ▶️'}</span></div> </div> <h4>Top Keywords</h4> <div>${topKWs.map(([k,w])=>`<span class="igpa-chip">${k.slice(3)} <span class="igpa-small">${w.toFixed(2)}</span></span>`).join('')}</div> <h4>Top Hashtags</h4> <div>${topTags.map(([k,w])=>`<span class="igpa-chip">#${k.slice(4)} <span class="igpa-small">${w.toFixed(2)}</span></span>`).join('')}</div> <h4>Top Creators</h4> <div>${topUsers.map(([k,w])=>`<span class="igpa-chip">@${k.slice(5)} <span class="igpa-small">${w.toFixed(2)}</span></span>`).join('')}</div> `; } else if (name==='session'){ const last=STATE.history.slice(-220).reverse(); const csvBtn = mkBtn('Export CSV', 'igpa-btn--ghost', {icon:mkIcon('M5 20h14v-2H5v2zm7-18l-5.5 5.5h3.5V15h4V7.5H18L12 2z')}); csvBtn.addEventListener('click',exportHistoryCSV); bodyEl.innerHTML=''; bodyEl.append(csvBtn); if (!last.length){ const p=document.createElement('p'); p.className='igpa-small'; p.textContent='No events yet — scroll or interact to generate data.'; bodyEl.append(p); return; } last.forEach(ev=>{ const row=document.createElement('div'); row.className='igpa-session'; row.style.borderBottom='1px solid var(--igpa-border)'; row.style.padding='6px 0'; const t=document.createElement('div'); t.className='igpa-small'; t.style.opacity='.7'; t.textContent=new Date(ev.ts).toLocaleTimeString(); const mid=document.createElement('div'); const lbl=(ev.label>=0.999?'Positive':(ev.label<=0.001?'Negative':`Label ${ev.label.toFixed(2)}`)); mid.innerHTML=`<span class="igpa-pill">${lbl}</span> <span class="igpa-small">via ${ev.via||'watch'}</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>`; const ft=document.createElement('div'); ft.className='igpa-small'; ft.style.marginTop='2px'; ft.style.color=ev.label>=0.5?'var(--igpa-success)':'var(--igpa-danger)'; ft.textContent=(ev.feats||[]).map(f=>`${prettyFeat(f.k)}:${(f.contrib||0).toFixed(2)}`).join(' · '); mid.append(ft); const rt=document.createElement('div'); rt.className='igpa-small'; rt.style.textAlign='right'; rt.textContent=ev.seconds!=null?`${ev.seconds.toFixed(1)}s`:''; row.append(t,mid,rt); bodyEl.append(row); }); } else if (name==='weights'){ bodyEl.innerHTML = ` <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;"> <input type="text" id="igpa-wsearch" placeholder="Search @creator, #hashtag, keyword, feature..." 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 igpa-btn--danger" id="igpa-zero">Zero Selected</button> </div> <div id="igpa-wlist"></div> `; const list = bodyEl.querySelector('#igpa-wlist'); const input = bodyEl.querySelector('#igpa-wsearch'); const renderList = ()=>{ const q=(input.value||'').trim().toLowerCase(); const items=Object.entries(STATE.weights).filter(([k])=>k!=='__bias'&&(q? k.toLowerCase().includes(q):true)).sort((a,b)=>Math.abs(b[1])-Math.abs(a[1])); list.innerHTML = items.slice(0,420).map(([k,w])=>` <div class="igpa-row" data-key="${k}"> <div style="max-width:60%">${prettyFeat(k)}</div> <div class="igpa-small">${w.toFixed(3)}</div> <div> <button class="igpa-btn" data-delta="0.05">+0.05</button> <button class="igpa-btn" data-delta="-0.05">-0.05</button> <button class="igpa-btn igpa-btn--ghost" data-delta="zero">Zero</button> </div> </div> `).join('') || '<p class="igpa-small">No weights (train the model first).</p>'; list.querySelectorAll('.igpa-row .igpa-btn').forEach(btn=>{ attachRipple(btn); btn.onclick=()=>{ const row=btn.closest('.igpa-row'); const key=row.getAttribute('data-key'); const d=btn.getAttribute('data-delta'); if (d==='zero') STATE.weights[key]=0; else STATE.weights[key]=(STATE.weights[key]||0)+Number(d); persist(); renderList(); }; }); }; input.oninput=renderList; bodyEl.querySelector('#igpa-zero').onclick=()=>{ const q=(input.value||'').trim().toLowerCase(); if(!q) return; Object.keys(STATE.weights).forEach(k=>{ if(k!=='__bias'&&k.toLowerCase().includes(q)) STATE.weights[k]=0; }); persist(); renderList(); }; renderList(); } else if (name==='prefs'){ bodyEl.innerHTML=` <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;"> <input type="text" id="igpa-pref-key" 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="igpa-pref-type" 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> </select> <button class="igpa-btn igpa-btn--primary" id="igpa-pref-add">Add</button> </div> <h4>Pinned</h4> <div id="igpa-pref-pins"></div> <h4>Banned</h4> <div id="igpa-pref-bans"></div> `; const renderList=(sel, obj, kind)=>{ const box=bodyEl.querySelector(sel); const keys=Object.keys(obj); box.innerHTML = keys.length? keys.map(v=>{ const lab = (sel.includes('pins') && sel.includes('pref'))? v : v; const chip=document.createElement('span'); chip.className='igpa-chip'; chip.textContent=(sel.includes('hashtags')||v.startsWith('#'))?('#'+v):v; const del=mkBtn('', 'igpa-btn--icon igpa-btn--danger', {icon:mkIcon('M6 6l12 12M18 6L6 18'), tooltip:'Remove'}); del.addEventListener('click',()=>{ if(kind==='pin') deletePref(lab,'pin'); else deletePref(lab,'ban'); renderAll(); }); chip.appendChild(del); return chip.outerHTML; }).join('') : '<p class="igpa-small">Empty.</p>'; }; const deletePref=(v,kind)=>{ if (v.startsWith('@')){ const u=v.replace(/^@/,'').toLowerCase(); if(kind==='pin') delete STATE.pins.creators[u]; else delete STATE.bans.creators[u]; } else if (v.startsWith('#')){ const t=v.replace(/^#/,'').toLowerCase(); if(kind==='pin') delete STATE.pins.hashtags[t]; else { delete STATE.bans.hashtags[t]; delete STATE.mutedHashtags[t]; } } else { const k=v.toLowerCase(); if(kind==='pin') delete STATE.pins.keywords[k]; else delete STATE.bans.keywords[k]; } persist(); }; const renderAll=()=>{ const pinsC=STATE.pins.creators, pinsH=STATE.pins.hashtags, pinsK=STATE.pins.keywords; const bansC=STATE.bans.creators, bansH=STATE.bans.hashtags, bansK=STATE.bans.keywords; bodyEl.querySelector('#igpa-pref-pins').innerHTML = [...Object.keys(pinsC).map(u=>'@'+u), ...Object.keys(pinsH).map(t=>'#'+t), ...Object.keys(pinsK)].map(v=>{ const chip=document.createElement('span'); chip.className='igpa-chip'; chip.textContent=v; const del=mkBtn('', 'igpa-btn--icon igpa-btn--danger', {icon:mkIcon('M6 6l12 12M18 6L6 18'), tooltip:'Remove'}); del.addEventListener('click',()=>{ deletePref(v,'pin'); renderAll(); }); chip.appendChild(del); return chip.outerHTML; }).join('') || '<p class="igpa-small">No pins.</p>'; bodyEl.querySelector('#igpa-pref-bans').innerHTML = [...Object.keys(bansC).map(u=>'@'+u), ...Object.keys(bansH).map(t=>'#'+t), ...Object.keys(bansK)].map(v=>{ const chip=document.createElement('span'); chip.className='igpa-chip'; chip.textContent=v; const del=mkBtn('', 'igpa-btn--icon igpa-btn--danger', {icon:mkIcon('M6 6l12 12M18 6L6 18'), tooltip:'Remove'}); del.addEventListener('click',()=>{ deletePref(v,'ban'); renderAll(); }); chip.appendChild(del); return chip.outerHTML; }).join('') || '<p class="igpa-small">No bans.</p>'; }; bodyEl.querySelector('#igpa-pref-add').onclick=()=>{ const raw=bodyEl.querySelector('#igpa-pref-key').value.trim(); const kind=bodyEl.querySelector('#igpa-pref-type').value; if(!raw) return; if (raw.startsWith('@')){ const u=raw.slice(1).toLowerCase(); (kind==='pin'?STATE.pins.creators:STATE.bans.creators)[u]=true; } else if (raw.startsWith('#')){ const t=raw.slice(1).toLowerCase(); (kind==='pin'?STATE.pins.hashtags:STATE.bans.hashtags)[t]=true; if(kind==='ban') STATE.mutedHashtags[t]=true; } else { const k=raw.toLowerCase(); (kind==='pin'?STATE.pins.keywords:STATE.bans.keywords)[k]=true; } bodyEl.querySelector('#igpa-pref-key').value=''; persist(); renderAll(); }; renderAll(); } else if (name==='settings'){ bodyEl.innerHTML=` <div class="igpa-row"><div>Theme</div> <div> <select id="igpa-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> ${sliderRow('LR A','lrA',CONFIG.learn.lrA,0.02,0.35,0.01)} ${sliderRow('LR B','lrB',CONFIG.learn.lrB,0.02,0.35,0.01)} ${sliderRow('Low threshold','lowScoreThreshold',STATE.ui.lowScoreThreshold,0.05,0.85,0.05)} ${sliderRow('High threshold','highScoreThreshold',STATE.ui.highScoreThreshold,0.5,0.98,0.01)} ${sliderRow('Watch cap (s)','maxWatchImpactSec',CONFIG.learn.maxWatchImpactSec,10,90,5)} ${sliderRow('Image watch weight','watchWeightImage',CONFIG.learn.watchWeightImage,0.4,1.6,0.05)} ${sliderRow('Video watch weight','watchWeightVideo',CONFIG.learn.watchWeightVideo,0.4,1.8,0.05)} ${toggleRow('Show badge','showBadge',STATE.ui.showBadge)} ${toggleRow('Compact badge','compactBadge',STATE.ui.compactBadge)} ${toggleRow('Explain top feats','showExplain',STATE.ui.showExplain)} ${toggleRow('Dim low-score','dimLowScore',STATE.ui.dimLowScore)} ${toggleRow('Blur low-score','blurLowScore',STATE.ui.blurLowScore)} ${toggleRow('Highlight high-score','highlightHighScore',STATE.ui.highlightHighScore)} ${toggleRow('Eye-comfort blur','eyeComfort',STATE.ui.eyeComfort)} ${toggleRow('Cap creator influence','capCreator',STATE.ui.capCreator)} ${toggleRow('Pause learning','pauseLearning',STATE.ui.pauseLearning)} ${toggleRow('Debug overlay','showDebugOverlay',STATE.ui.showDebugOverlay)} ${toggleRow('Auto-skip low','autoSkipLow',STATE.ui.autoSkipLow)} <div class="igpa-row"><div>Auto-skip delay (ms)</div><input class="igpa-range" data-key="autoSkipDelayMs" type="range" min="250" max="2000" step="50" value="${STATE.ui.autoSkipDelayMs}"><div class="igpa-small">${STATE.ui.autoSkipDelayMs}</div></div> <div class="igpa-row"><div>Autoskip jitter (ms)</div><input class="igpa-range" data-key="autoskipJitterMax" type="range" min="0" max="1000" step="20" value="${STATE.ui.autoskipJitterMax}"><div class="igpa-small">${STATE.ui.autoskipJitterMax}</div></div> <hr/> <div style="display:flex; gap:8px; flex-wrap:wrap;"> <button class="igpa-btn" id="igpa-hide-low">Hide low-score in view</button> <button class="igpa-btn igpa-btn--ghost" id="igpa-show-all">Show all</button> <button class="igpa-btn igpa-btn--primary" id="igpa-jump-next">Jump High (J)</button> <button class="igpa-btn" id="igpa-jump-prev">Jump High Prev (K)</button> </div> <p class="igpa-small" style="opacity:.8;margin-top:6px">Auto-scrolling might be considered automated behavior. Use carefully.</p> `; const themeSel=bodyEl.querySelector('#igpa-theme'); themeSel.value=STATE.ui.theme; themeSel.onchange=()=>{ STATE.ui.theme=themeSel.value; persist(); setThemeAttr(); }; // 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==='lowScoreThreshold'){ STATE.ui.lowScoreThreshold=val; persist(); applyVisualFilters(); } else if (key==='highScoreThreshold'){ STATE.ui.highScoreThreshold=val; persist(); applyVisualFilters(); } else if (key==='autoSkipDelayMs'){ STATE.ui.autoSkipDelayMs=val; persist(); } else if (key==='autoskipJitterMax'){ STATE.ui.autoskipJitterMax=val; persist(); } else if (key==='maxWatchImpactSec'){ CONFIG.learn.maxWatchImpactSec=val; persist(); } else if (key==='watchWeightImage'){ CONFIG.learn.watchWeightImage=val; persist(); } else if (key==='watchWeightVideo'){ CONFIG.learn.watchWeightVideo=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(); DEBUG=!!STATE.ui.showDebugOverlay; setThemeAttr(); applyVisualFilters(); }); }); // quick buttons bodyEl.querySelector('#igpa-hide-low').onclick=()=>applyVisualFilters(true); bodyEl.querySelector('#igpa-show-all').onclick=()=>applyVisualFilters(false,true); bodyEl.querySelector('#igpa-jump-next').onclick=()=>jumpHighScore('next'); bodyEl.querySelector('#igpa-jump-prev').onclick=()=>jumpHighScore('prev'); } 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="igpa-export">Export JSON</button> <input type="file" id="igpa-import" 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 igpa-btn--danger" id="igpa-reset">Reset</button> </div> <h4>Snapshots</h4> <div style="display:flex; gap:8px; margin:6px 0;"> <input id="igpa-snap-name" 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="igpa-snap-save">Save</button> </div> <div id="igpa-snap-list"></div> `; bodyEl.querySelector('#igpa-export').onclick=exportJSON; bodyEl.querySelector('#igpa-import').onchange=(e)=>importJSON(e.target.files[0]); bodyEl.querySelector('#igpa-reset').onclick=resetAll; bodyEl.querySelector('#igpa-snap-save').onclick=saveSnapshot; renderSnapshots(); } else if (name==='help'){ bodyEl.innerHTML=` <p>Local-only learner. Topic model + hashtags + creator + media type + <strong>watch-time</strong> (image exposure & video play) → continuous labels.</p> <ul> <li><strong>Shortcuts:</strong> J/K next/prev high-score, H toggle autoskip, Ctrl+Shift+I panel.</li> <li><strong>Training:</strong> <${CONFIG.learn.minNegWatchSec}s → negative, ≥${CONFIG.learn.minPosWatchSec}s → positive; like/save adds a small boost.</li> <li><strong>Buttons:</strong> Ripple, tooltips, toggles, segmented filters in toolbar.</li> <li><strong>Overexposure:</strong> soft penalty when one creator dominates recent feed.</li> </ul> `; } } const sliderRow=(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 toggleRow=(label,key,val)=>`<div class="igpa-row"><div>${label}</div><input type="checkbox" data-key="${key}" ${val?'checked':''}></div>`; /* ========================= Badge + Quick Actions (new stylish buttons + context) ==========================*/ function currentMeta(el){ return { creator:(getUsername(el)||'unknown'), caption:getCaptionText(el) }; } function explainTop(el){ try{ const feats=featuresFromPost(el); const top=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 top.map(([k,w])=>k.startsWith('tag:')?`#${k.slice(4)}:${w.toFixed(2)}`:k.startsWith('user:')?`@${k.slice(5)}:${w.toFixed(2)}`:k.startsWith('kw:')?`${k.slice(3)}:${w.toFixed(2)}`:`${k}:${w.toFixed(2)}`).join(' · '); }catch{return'';} } function paintBadge(el){ try{ let badge=el.querySelector(':scope > .igpa-badge'); const score=predictScore(el); if (!STATE.ui.showBadge){ if(badge) badge.remove(); return; } 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(score*100); const explain=STATE.ui.showExplain?explainTop(el):''; const color=score>=STATE.ui.highScoreThreshold?'var(--igpa-accent)':(score<STATE.ui.lowScoreThreshold?'#999':'#ddd'); const metaC=(getUsername(el)||'unknown'); const cap=currentMeta(el).caption; const tags=extractHashtags(cap).slice(0,2); const kws=tokenize(cap).slice(0,2); const btns=[...tags.map(t=>`<button class="mini-btn" data-act="boost-tag" data-tag="${t}">#${t} ⊕</button><button class="mini-btn" data-act="mute-tag" data-tag="${t}">#${t} ⊖</button>`), ...kws.map(s=>`<button class="mini-btn" data-act="boost-kw" data-kw="${s}">${s} ⊕</button><button class="mini-btn" data-act="mute-kw" data-kw="${s}">${s} ⊖</button>`)].join(''); badge.innerHTML = STATE.ui.compactBadge ? `<span class="pct" style="color:${color}">${pct}</span>` : `<span class="pct" style="color:${color}">${pct}</span> <span class="igpa-small">% match</span>${explain?`<span class="exp">${explain}</span>`:''} <div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:4px;"> <button class="mini-btn" data-act="pin-user">@${metaC} ⊕</button> <button class="mini-btn" data-act="ban-user">@${metaC} ⊖</button> ${btns} </div>`; badge.onclick=(ev)=>{ const b=ev.target.closest('.mini-btn'); if(!b) return; const act=b.getAttribute('data-act'); if (act==='pin-user'){ STATE.pins.creators[metaC]=true; persist(); tip(`Pinned @${metaC}`); } else if (act==='ban-user'){ STATE.bans.creators[metaC]=true; persist(); tip(`Banned @${metaC}`); } else if (act==='boost-tag'){ const t=b.getAttribute('data-tag'); STATE.pins.hashtags[t]=true; persist(); tip(`Boosted #${t}`); } else if (act==='mute-tag'){ const t=b.getAttribute('data-tag'); STATE.bans.hashtags[t]=true; STATE.mutedHashtags[t]=true; persist(); tip(`Muted #${t}`); applyVisualFilters(); } else if (act==='boost-kw'){ const k=b.getAttribute('data-kw'); STATE.pins.keywords[k]=true; persist(); tip(`Boosted "${k}"`); } else if (act==='mute-kw'){ const k=b.getAttribute('data-kw'); STATE.bans.keywords[k]=true; persist(); tip(`Muted "${k}"`); } ev.stopPropagation(); }; // Post filters const muted=tags.some(t=>STATE.mutedHashtags[t]); const sponsored=STATE.ui.eyeComfort && likelySponsored(el); el.classList.remove('igpa-muted','igpa-blur','igpa-highlight'); if (STATE.ui.dimLowScore && score<STATE.ui.lowScoreThreshold) el.classList.add('igpa-muted'); if ((STATE.ui.blurLowScore && score<STATE.ui.lowScoreThreshold) || (sponsored||muted)) el.classList.add('igpa-blur'); if (STATE.ui.highlightHighScore && score>=STATE.ui.highScoreThreshold) el.classList.add('igpa-highlight'); // Debug overlay small if (STATE.ui.showDebugOverlay) { let dbg=el.querySelector(':scope > .igpa-badge-debug'); if(!dbg){ dbg=document.createElement('div'); dbg.className='igpa-badge-debug'; dbg.style.position='absolute'; dbg.style.left='8px'; dbg.style.top='8px'; dbg.style.padding='4px 6px'; dbg.style.fontSize='10px'; dbg.style.background='rgba(0,0,0,.45)'; dbg.style.color='#fff'; dbg.style.borderRadius='6px'; dbg.style.zIndex=String(CONFIG.ui.zIndex-1); el.appendChild(dbg); } const id=getPostIdFromEl(el), rec=activeTimers.get(id); let vsecs=0; el.querySelectorAll(SELECTORS.video).forEach(v=>{ const tr=videoTrackers.get(v); if (tr) vsecs+=tr.playAccum+(tr.playing? (now()-tr.lastTick)/1000:0);} ); const secs=(rec? (rec.accum+(rec.start?(now()-rec.start)/1000:0)) :0).toFixed(1); dbg.textContent=`img:${secs}s vid:${vsecs.toFixed(1)}s`; } else { el.querySelector(':scope > .igpa-badge-debug')?.remove(); } tryColorStats(el,badge); 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 bUp=document.createElement('button'); bUp.className='igpa-qbtn'; attachTooltip(bUp,'Thumb up'); attachRipple(bUp); bUp.innerHTML=mkIcon('M2 12h4v8H2v-8zm6 8h8a2 2 0 0 0 2-2v-5c0-.55-.45-1-1-1h-5.31l.95-4.57.02-.18a1 1 0 0 0-1.97-.25L9 9H6v11z').outerHTML; const bDn=document.createElement('button'); bDn.className='igpa-qbtn'; attachTooltip(bDn,'Thumb down'); attachRipple(bDn); bDn.innerHTML=mkIcon('M22 12h-4V4h4v8zM16 4H8a2 2 0 0 0-2 2v5c0 .55.45 1 1 1h5.31l-.95 4.57-.02.18a1 1 0 0 0 1.97.25L15 15h3V4z').outerHTML; const bHide=document.createElement('button'); bHide.className='igpa-qbtn'; attachTooltip(bHide,'Hide post'); attachRipple(bHide); bHide.innerHTML=mkIcon('M12 5c7.633 0 11 7 11 7s-3.367 7-11 7S1 12 1 12s3.367-7 11-7zm0 3a4 4 0 1 0 .001 8.001A4 4 0 0 0 12 8z').outerHTML; foot.append(bUp,bDn,bHide); el.appendChild(foot); bUp.onclick=()=>manualTrain(el,1,'thumb'); bDn.onclick=()=>manualTrain(el,0,'thumb'); bHide.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(SELECTORS.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, normalizedWatchLabel(sec,getPostType(el)))); const feats=featuresFromPost(el,{watchBucket:watchBucket(sec)}); const upd=sgdUpdate(feats,l); const creator=getUsername(el)||'unknown'; if (l>=0.5) STATE.stats.positives++; else STATE.stats.negatives++; addHistory({ts:Date.now(),id,metaCreator:creator,label:l,via,seconds:sec||null,pred:upd.pred,loss:upd.loss,arm:upd.arm,lr:upd.lrUsed,feats:pickTopFeats(feats)}); persist(); paintBadge(el); } /* ========================= Feed observers + color chip ==========================*/ let mo=null; function ensureMO(){ if (mo) return mo; mo = new MutationObserver(muts=>{ try{ const added=[]; for (const m of muts){ for (const node of m.addedNodes){ if (node.nodeType!==1) continue; const el=/** @type {HTMLElement} */(node); if (isPost(el)) added.push(el); el.querySelectorAll?.(SELECTORS.article).forEach(x=>{ if (isPost(x)) added.push(x); }); } } const seen=new Set(); for (const el of added){ if(seen.has(el)) continue; seen.add(el); preparePost(el); } }catch(e){ STATE.status.lastError='mo:'+String(e); } }); return mo; } function preparePost(el){ try{ if (el.dataset?.igpaReady==='1') return; if (el.dataset) el.dataset.igpaReady='1'; ensureIO().observe(el); paintBadge(el); attachVideoTracker(el, getPostIdFromEl(el)); STATE.status.observing=true; }catch(e){ STATE.status.lastError='prep:'+String(e); } } const COLOR_CACHE_MAX=900; function vacuumColorCache(){ const keys=Object.keys(STATE.colorCache); if (keys.length<=COLOR_CACHE_MAX) return; keys.sort((a,b)=>STATE.colorCache[a].ts-STATE.colorCache[b].ts); for (let i=0;i<keys.length-COLOR_CACHE_MAX;i++) delete STATE.colorCache[keys[i]]; } function tryColorStats(el,badge){ try{ const img=el.querySelector('img'); if(!img||!img.src) return; const key=hashStr(img.src); if (STATE.colorCache[key]){ addChip(STATE.colorCache[key].avg); return; } const onload=()=>{ try{ const sameOrigin=img.src.startsWith(location.origin) || img.crossOrigin==='anonymous'; if(!sameOrigin) return; const c=document.createElement('canvas'), ctx=c.getContext('2d'); const w=Math.min(48,img.naturalWidth||48), h=Math.min(48,img.naturalHeight||48); c.width=w; c.height=h; ctx.drawImage(img,0,0,w,h); const d=ctx.getImageData(0,0,w,h).data; let r=0,g=0,b=0,n=0; for(let i=0;i<d.length;i+=4){ r+=d[i]; g+=d[i+1]; b+=d[i+2]; n++; } const avg=[Math.round(r/n),Math.round(g/n),Math.round(b/n)]; STATE.colorCache[key]={avg,ts:Date.now()}; vacuumColorCache(); persist(); addChip(avg); }catch{} }; function addChip(avg){ try{ const holder=badge||el.querySelector(':scope > .igpa-badge'); if(!holder) return; const chip=document.createElement('span'); chip.className='igpa-colorchip'; chip.style.display='inline-block'; chip.style.width='12px'; chip.style.height='12px'; chip.style.borderRadius='3px'; chip.style.border='1px solid rgba(255,255,255,.2)'; chip.style.marginLeft='6px'; chip.style.background=`rgb(${avg.join(',')})`; holder.appendChild(chip); }catch{} } if (img.complete) onload(); else img.addEventListener('load', onload, {once:true}); }catch{} } /* ========================= Navigation helpers ==========================*/ function jumpHighScore(dir='next'){ try{ const posts=[...document.querySelectorAll(SELECTORS.article)].filter(isPost); const y=window.scrollY; const candidates=posts.map(el=>({el,score:predictScore(el),top:el.getBoundingClientRect().top+window.scrollY})) .filter(p=>p.score>=STATE.ui.highScoreThreshold).sort((a,b)=>a.top-b.top); if(!candidates.length){ tip('No high-score posts in view.'); return; } if(dir==='next'){ const cand=candidates.find(p=>p.top>y+60) || candidates[0]; window.scrollTo({top:cand.top-80,behavior:'smooth'}); } else { const cand=[...candidates].reverse().find(p=>p.top<y-60) || candidates[candidates.length-1]; window.scrollTo({top:cand.top-80,behavior:'smooth'}); } }catch{} } function scrollToNextHighScore(){ jumpHighScore('next'); } /* ========================= Data helpers ==========================*/ function addHistory(ev){ const h=STATE.history; h.push(ev); if(h.length>CONFIG.learn.maxEvents) h.shift(); if (ev.label>=0.5) STATE.stats.positives++; else STATE.stats.negatives++; } function weightSubset(prefix){ const o={}; for(const k in STATE.weights) if(k.startsWith(prefix)) o[k]=STATE.weights[k]; return o; } function topN(objOrWeights, n=10){ const entries=Array.isArray(objOrWeights)?objOrWeights:Object.entries(objOrWeights); return entries.filter(([k])=>k!=='__bias').sort((a,b)=>Math.abs(b[1])-Math.abs(a[1])).slice(0,n); } function prettyFeat(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 exportJSON(){ const snapshot={version:CONFIG.version,state:STATE}; const blob=new Blob([JSON.stringify(snapshot,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='ig-preference-ai-luxe.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(url),4e3); } async function importJSON(file){ if(!file) return; const text=await file.text(); try{ const obj=JSON.parse(text); const st=obj.state||obj; if(!st||!st.weights||!st.ui) throw new Error('Invalid'); STATE=Object.assign({},DEFAULT_STATE,st); persist(); applyVisualFilters(false,true); tip('Imported.'); }catch{ tip('Invalid JSON.'); } } function saveSnapshot(){ const name=bodyEl?.querySelector('#igpa-snap-name')?.value?.trim(); if(!name){ tip('Name your snapshot.'); return; } const snap={version:CONFIG.version,weights:STATE.weights,ui:STATE.ui,pins:STATE.pins,bans:STATE.bans,mutedHashtags:STATE.mutedHashtags,ts:Date.now()}; STATE.snapshots[name]=JSON.parse(JSON.stringify(snap)); persist(); renderSnapshots(); tip(`Saved "${name}"`); } function renderSnapshots(){ const box=bodyEl?.querySelector('#igpa-snap-list'); 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-snap="${n}" data-act="load">Load</button> <button class="igpa-btn igpa-btn--danger" data-snap="${n}" data-act="del">Delete</button> </div> </div>`).join('') : '<p class="igpa-small">No snapshots yet.</p>'; box.querySelectorAll('[data-snap]').forEach(btn=>{ btn.onclick=()=>{ const n=btn.getAttribute('data-snap'); const act=btn.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)); persist(); applyVisualFilters(false,true); tip(`Loaded "${n}"`); } else { delete STATE.snapshots[n]; persist(); renderSnapshots(); } }; }); } function exportHistoryCSV(){ 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.metaCreator||'', 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),4e3); }catch{ tip('CSV export failed.'); } } function resetAll(){ if(!confirm('Reset model, preferences, and history?')) return; STATE=JSON.parse(JSON.stringify(DEFAULT_STATE)); persist(); applyVisualFilters(false,true); tip('Reset complete.'); } /* ========================= Filters, tip, boot ==========================*/ let raf=null; function applyVisualFilters(hide=false, showAll=false){ if (raf) cancelAnimationFrame(raf); raf=requestAnimationFrame(()=>{ try{ document.querySelectorAll(SELECTORS.article).forEach(el=>{ if(!isPost(el)) return; const s=predictScore(el); if (showAll) el.style.display=''; else if (hide && s<STATE.ui.lowScoreThreshold) el.style.display='none'; else el.style.display=''; paintBadge(el); }); } finally { raf=null; } }); } 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(), 1200); } function keepPanelInViewport(){ try{ if(!panel) return; const r=panel.getBoundingClientRect(); let nx=r.left, ny=r.top; const pad=8; if (r.right>window.innerWidth-pad) nx-=(r.right-(window.innerWidth-pad)); if (r.bottom>window.innerHeight-pad) ny-=(r.bottom-(window.innerHeight-pad)); if (r.left<pad) nx=pad; if (r.top<pad) ny=pad; panel.style.left=nx+'px'; panel.style.top=ny+'px'; persistPanelPos(); }catch{} } window.addEventListener('resize', keepPanelInViewport); async function boot(){ try{ buildUI(); let attempts=0; while (attempts<140){ const ready=document.querySelector(SELECTORS.postAnchor)||document.querySelector(SELECTORS.article); if (ready) break; await sleep(100); attempts++; } ensureMO().observe(document.body,{childList:true,subtree:true}); document.querySelectorAll(SELECTORS.article).forEach(el=>{ if(isPost(el)) preparePost(el); }); setInterval(()=>{ STATE.status.observing=!!io; },1500); STATE.status.booted=true; persist(); renderTab('overview'); log('initialized Luxe'); }catch(e){ STATE.status.lastError='boot:'+String(e); persist(); warn('Boot error',e); } } // Fire it up setThemeAttr(); boot(); })();