Greasy Fork is available in English.
lightweight quiz helper
// ==UserScript==
// @name Feather Client
// @namespace https://github.com/vxinne/feather
// @version 2.0.0
// @description lightweight quiz helper
// @author Xandros and Vxinne
// @license GPL-3.0
// @match https://quizizz.com/*
// @match https://wayground.com/*
// @match https://*.quizizz.com/*
// @match https://*.wayground.com/*
// @match https://exam.preahsisowath.edu.kh/*
// @match https://*.preahsisowath.edu.kh/*
// @match https://docs.google.com/forms/*
// @grant GM_addStyle
// @grant GM_log
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @connect *
// @run-at document-start
// ==/UserScript==
(function () {
"use strict";
/* ╔═══════════════════════════════════════════╗
║ §1 CONFIG ║
╚═══════════════════════════════════════════╝ */
const K = { KEY: "FL_GK", UI: "fl_ui", CFG: "FL_CFG", FIRST: "FL_1ST", CACHE: "FL_QC" };
const MONO = "'SF Mono','Cascadia Code','Fira Code','JetBrains Mono',Consolas,monospace";
const REAL_MODEL = 'gemini-3-flash-preview';
const MODEL_DISPLAY = 'Gemini 3 Flash';
const DEFAULTS = {
geminiApiKey: "",
enableSpoofFullscreen: true,
enableAutoAnswer: false,
autoAnswerDelay: 1800,
timeTakenMin: 5500,
timeTakenMax: 9000,
enableTimeTakenEdit: true,
enableTimerHijack: false,
timerBonusPoints: 270,
enableReactionSpam: false,
reactionSpamCount: 1,
reactionSpamDelay: 2000,
includeImages: true,
thinkingBudget: 512,
maxOutputTokens: 1024,
temperature: 0.15,
serverUrl: "https://uets.meowery.eu",
selectedModel: 'gemini-3-flash-preview',
formsModel: 'gemini-3-flash-preview',
};
const PROFILES = {
stealth: { enableTimeTakenEdit:false, enableTimerHijack:false, enableSpoofFullscreen:true, enableReactionSpam:false, enableAutoAnswer:false },
extended: { enableTimeTakenEdit:true, timeTakenMin:8000, timeTakenMax:14000, enableTimerHijack:true, timerBonusPoints:200, enableSpoofFullscreen:true, enableReactionSpam:false, enableAutoAnswer:false },
creator: { enableTimeTakenEdit:true, timeTakenMin:6000, timeTakenMax:8000, enableTimerHijack:true, timerBonusPoints:270, enableSpoofFullscreen:true, enableReactionSpam:false, enableAutoAnswer:true, autoAnswerDelay:2000 },
lmao: { enableTimeTakenEdit:true, timeTakenMin:1000, timeTakenMax:2000, enableTimerHijack:true, timerBonusPoints:5000, enableSpoofFullscreen:true, enableReactionSpam:true, reactionSpamCount:2, reactionSpamDelay:500, enableAutoAnswer:true, autoAnswerDelay:500 },
};
const MODELS = {
'gemini-3-pro-preview': { name: 'Gemini 3 Pro', speed: 'medium' },
'gemini-3-flash-preview': { name: 'Gemini 3 Flash', speed: 'fast' },
'gemini-2.5-flash': { name: 'Gemini 2.5 Flash', speed: 'fast' },
'gemini-2.5-pro': { name: 'Gemini 2.5 Pro', speed: 'slow' },
'gemini-2.0-flash': { name: 'Gemini 2.0 Flash', speed: 'fastest' },
};
/* ╔═══════════════════════════════════════════╗
║ §2 STATE ║
╚═══════════════════════════════════════════╝ */
const S = {
on: GM_getValue(K.UI, true),
config: { ...DEFAULTS, ...GM_getValue(K.CFG, {}) },
panel: null, toastEl: null, toastTimer: null, orbEl: null,
observer: null, qData: {}, detected: {}, curQId: null,
autoAnswerPending: false,
answerCache: GM_getValue(K.CACHE, {}),
accuracyCount: { correct: 0, total: 0 },
};
if (!S.config.geminiApiKey) S.config.geminiApiKey = GM_getValue(K.KEY, "");
/* ╔═══════════════════════════════════════════╗
║ §3 STYLES — silver & white ║
╚═══════════════════════════════════════════╝ */
const isGF = () => location.hostname.includes('docs.google.com') && location.pathname.includes('/forms/');
const isExam = () => location.hostname.includes('preahsisowath.edu.kh');
GM_addStyle(`
:root {
--bg: #0c0c0c;
--bg1: #111111;
--bg2: #161616;
--bg3: #1c1c1c;
--line: #242424;
--dim: #383838;
--mid: #606060;
--si: #8a8a8a; /* silver mid */
--sh: #b8b8b8; /* silver hi */
--fg: #d8d8d8;
--wh: #f0f0f0;
--pt: #e8e8e8; /* platinum */
--lm: ${MONO};
}
/* ── ORB: nearly invisible, stealth ── */
#fl-orb {
position: fixed; bottom: 28px; left: 28px;
width: 28px; height: 28px; line-height: 28px; text-align: center;
font-family: var(--lm); font-size: 11px; color: var(--dim);
cursor: pointer; z-index: 2147483647; user-select: none;
opacity: .12; transition: opacity .3s, color .3s;
background: transparent; border: none;
}
#fl-orb:hover { opacity: .6; color: var(--sh); }
#fl-orb.off { opacity: .05; }
/* ── PANEL ── */
.fl-panel {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%,-50%);
background: var(--bg);
border: 1px solid var(--line);
color: var(--fg); font-family: var(--lm); font-size: 12px;
z-index: 2147483647; max-height: 88vh;
display: flex; flex-direction: column; min-width: 500px;
animation: flIn .18s cubic-bezier(.16,1,.3,1);
box-shadow:
0 0 0 1px var(--bg2),
0 40px 100px rgba(0,0,0,.97),
inset 0 1px 0 rgba(255,255,255,.03);
}
@keyframes flIn {
from { opacity:0; transform:translate(-50%,-46%) scale(.96); }
to { opacity:1; transform:translate(-50%,-50%) scale(1); }
}
/* ── HEADER ── */
.fl-hdr {
padding: 20px 24px 14px;
border-bottom: 1px solid var(--line);
user-select: none; position: relative;
background: linear-gradient(180deg, #0f0f0f 0%, var(--bg) 100%);
}
/* platinum shimmer line */
.fl-hdr::after {
content: '';
position: absolute; top: 0; left: 20%; right: 20%; height: 1px;
background: linear-gradient(90deg,
transparent,
rgba(232,232,232,0.12),
rgba(232,232,232,0.28),
rgba(232,232,232,0.12),
transparent
);
}
.fl-hdr-title {
display: block;
font-size: 13px; letter-spacing: .65em;
color: var(--pt); font-weight: normal;
margin-bottom: 5px;
}
.fl-hdr-sub {
font-size: 9px; letter-spacing: .28em;
color: var(--dim); text-transform: uppercase;
}
.fl-hdr-model {
font-size: 9px; letter-spacing: .15em;
color: var(--mid); margin-top: 3px;
}
.fl-close {
position: absolute; top: 16px; right: 18px;
cursor: pointer; color: var(--dim);
font-size: 10px; letter-spacing: .1em;
transition: color .15s;
}
.fl-close:hover { color: var(--sh); }
/* ── BODY ── */
.fl-body { overflow-y: auto; padding: 0 22px 16px; flex: 1; }
.fl-body::-webkit-scrollbar { width: 1px; }
.fl-body::-webkit-scrollbar-thumb {
background: linear-gradient(var(--dim), var(--mid));
}
/* ── SECTION ── */
.fl-sec {
display: flex; align-items: center; gap: 10px;
margin: 16px 0 6px;
font-size: 8px; letter-spacing: .42em;
color: var(--dim); user-select: none; text-transform: uppercase;
}
.fl-sec::after { content: ''; flex: 1; height: 1px; background: var(--line); }
/* ── ROW ── */
.fl-row {
display: flex; align-items: center;
min-height: 26px; padding: 1px 0;
border-bottom: 1px solid transparent;
transition: border-color .1s;
}
.fl-row:hover { border-bottom-color: var(--bg3); }
.fl-lbl { color: var(--mid); flex: 1; font-size: 11px; letter-spacing: .03em; }
/* ── TOGGLE ── */
.fl-tog {
cursor: pointer; user-select: none;
font-size: 9px; letter-spacing: .14em;
color: var(--dim); padding: 2px 8px;
border: 1px solid var(--dim); min-width: 38px; text-align: center;
transition: color .15s, border-color .15s, background .15s;
}
.fl-tog:hover { color: var(--sh); border-color: var(--si); }
.fl-tog.on {
color: var(--pt); border-color: var(--sh);
background: rgba(232,232,232,.05);
text-shadow: 0 0 8px rgba(232,232,232,.2);
}
/* ── INPUT ── */
.fl-inp {
background: transparent; border: none;
border-bottom: 1px solid var(--line);
color: var(--fg); font-family: var(--lm);
font-size: 11px; width: 72px; text-align: right;
outline: 0; padding: 2px 0; transition: border-color .15s;
}
.fl-inp:focus { border-color: var(--sh); }
.fl-inp-w { width: 195px; text-align: left; }
/* ── SELECT ── */
.fl-sel {
background: var(--bg2); border: 1px solid var(--line);
color: var(--sh); font-family: var(--lm);
font-size: 10px; padding: 2px 6px;
outline: 0; cursor: pointer; transition: border-color .15s;
}
.fl-sel:focus { border-color: var(--sh); }
/* ── PROFILES ── */
.fl-prof-row { display: flex; gap: 5px; flex-wrap: wrap; padding: 5px 0 2px; }
.fl-prof {
cursor: pointer; color: var(--dim);
user-select: none; font-size: 8px; letter-spacing: .2em;
padding: 3px 10px; border: 1px solid var(--line);
transition: color .15s, border-color .15s;
}
.fl-prof:hover, .fl-prof.act { color: var(--pt); border-color: var(--sh); }
/* ── FOOTER ── */
.fl-foot {
display: flex; justify-content: flex-end; gap: 5px;
padding: 10px 22px; border-top: 1px solid var(--line);
background: #090909;
}
.fl-fb {
cursor: pointer; padding: 4px 14px;
border: 1px solid var(--line); background: transparent;
color: var(--mid); font-family: var(--lm);
font-size: 9px; letter-spacing: .14em;
transition: color .15s, border-color .15s, background .15s;
}
.fl-fb:hover { color: var(--fg); border-color: var(--si); }
.fl-fb.ok {
color: var(--pt); border-color: var(--sh);
background: rgba(232,232,232,.04);
box-shadow: 0 0 12px rgba(232,232,232,.06);
}
.fl-fb.ok:hover { box-shadow: 0 0 20px rgba(232,232,232,.12); }
/* ── TOAST ── */
.fl-toast {
position: fixed; top: 16px; right: 16px;
background: var(--bg);
border: 1px solid var(--line);
color: var(--sh); font-family: var(--lm); font-size: 11px;
z-index: 2147483647; max-width: 390px;
cursor: pointer; white-space: pre-wrap; line-height: 1.6;
transition: opacity .25s, transform .25s;
box-shadow:
0 0 0 1px var(--bg2),
0 16px 50px rgba(0,0,0,.97),
inset 0 1px 0 rgba(255,255,255,.03);
}
/* ── ACTION BUTTONS ── */
.fl-acts {
display: inline-flex !important; gap: 3px !important;
margin-top: 7px !important; flex-wrap: wrap !important;
position: relative !important; z-index: 99999 !important;
pointer-events: auto !important;
}
.fl-ab {
font-family: var(--lm) !important; font-size: 9px !important;
background: var(--bg) !important;
border: 1px solid var(--dim) !important;
color: var(--mid) !important; padding: 4px 11px !important;
cursor: pointer !important; text-decoration: none !important;
display: inline-block !important;
pointer-events: auto !important; position: relative !important;
z-index: 99999 !important; line-height: 1.4 !important;
user-select: none !important; letter-spacing: .1em !important;
transition: color .12s, border-color .12s, background .12s !important;
}
.fl-ab:hover {
color: var(--pt) !important; border-color: var(--sh) !important;
background: rgba(232,232,232,.04) !important;
}
.fl-ab.active { color: var(--sh) !important; border-color: var(--si) !important; }
/* ── ANSWER HIGHLIGHT ── */
.fl-hit {
outline: 1px solid rgba(232,232,232,.7) !important;
outline-offset: 3px !important;
box-shadow: 0 0 12px rgba(232,232,232,.08) !important;
position: relative !important;
}
.fl-hit-tag {
position: absolute; top: -8px; right: -8px;
background: var(--pt); color: var(--bg);
font-size: 9px; font-family: var(--lm);
padding: 1px 6px; z-index: 100; letter-spacing: .08em;
}
/* ── MISC ── */
.fl-auto-badge {
position: fixed; bottom: 28px; left: 68px;
font-family: var(--lm); font-size: 9px; color: var(--mid);
border: 1px solid var(--line); padding: 2px 8px;
z-index: 2147483646; user-select: none; opacity: .45; letter-spacing: .1em;
}
.fl-acc {
position: fixed; bottom: 28px; left: 148px;
font-family: var(--lm); font-size: 9px; color: var(--si);
z-index: 2147483646; user-select: none; opacity: .5;
}
`);
/* ╔═══════════════════════════════════════════╗
║ §4 HELPERS ║
╚═══════════════════════════════════════════╝ */
const $ = (s, p) => (p || document).querySelector(s);
const $$ = (s, p) => Array.from((p || document).querySelectorAll(s));
const randInt = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const plain = s => (s || '').replace(/\s+/g, ' ').trim();
const uid = () => Math.random().toString(36).slice(2, 8);
const isVis = el => !!(el && el.offsetParent !== null && el.offsetWidth > 0);
/* ╔═══════════════════════════════════════════╗
║ §5 TOAST ║
╚═══════════════════════════════════════════╝ */
const toast = (msg, loading = false) => {
if (S.toastTimer) clearTimeout(S.toastTimer);
if (S.toastEl) S.toastEl.remove();
if (S._loadingInterval) { clearInterval(S._loadingInterval); S._loadingInterval = null; }
const W = 44;
const rule = '─'.repeat(W);
const el = document.createElement('pre');
el.className = 'fl-toast';
if (loading) {
const frames = ['◐','◓','◑','◒'];
let fi = 0;
el.textContent = ` FEATHER · LITE\n${rule}\n ${frames[0]} ${msg}\n${rule}`;
document.body.appendChild(el);
S.toastEl = el;
S._loadingInterval = setInterval(() => {
fi = (fi + 1) % 4;
el.textContent = ` FEATHER · LITE\n${rule}\n ${frames[fi]} ${msg}\n${rule}`;
}, 160);
el.onclick = () => {
clearInterval(S._loadingInterval); S._loadingInterval = null;
el.remove(); S.toastEl = null;
};
return;
}
const lines = [];
for (const raw of msg.replace(/<[^>]+>/g, '').split('\n')) {
if (!raw) { lines.push(''); continue; }
const words = raw.split(' '); let cur = '';
for (const w of words) {
if ((cur + ' ' + w).trim().length > W - 4) { lines.push(cur); cur = w; }
else cur = cur ? cur + ' ' + w : w;
}
if (cur) lines.push(cur);
}
el.textContent = ` FEATHER · LITE\n${rule}\n${lines.map(l=>` ${l}`).join('\n')}\n${rule}`;
el.onclick = () => { if (S.toastTimer) clearTimeout(S.toastTimer); el.remove(); S.toastEl = null; };
document.body.appendChild(el);
S.toastEl = el;
S.toastTimer = setTimeout(() => {
el.style.opacity = '0'; el.style.transform = 'translateX(8px)';
setTimeout(() => { el.remove(); S.toastEl = null; }, 250);
}, 9000);
};
/* ╔═══════════════════════════════════════════╗
║ §6 GEMINI ENGINE ║
╚═══════════════════════════════════════════╝ */
const geminiCall = (promptText, images, apiKey, cfg = {}) => new Promise((resolve, reject) => {
const parts = [{ text: promptText }];
if (images?.length) {
for (const img of images) {
if (img.base64 && img.mimeType)
parts.push({ inline_data: { mime_type: img.mimeType, data: img.base64 } });
}
}
const genCfg = {
temperature: cfg.temp ?? S.config.temperature,
topP: 0.92, topK: 40,
maxOutputTokens: cfg.maxTokens ?? S.config.maxOutputTokens,
};
if (cfg.jsonMode) genCfg.responseMimeType = 'application/json';
GM_xmlhttpRequest({
method: 'POST',
url: `https://generativelanguage.googleapis.com/v1beta/models/${REAL_MODEL}:generateContent?key=${apiKey}`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ contents: [{ parts }], generationConfig: genCfg }),
onload: r => {
try {
const j = JSON.parse(r.responseText);
if (j.error) return reject(new Error(j.error.message));
const txt = j.candidates?.[0]?.content?.parts?.map(p => p.text).filter(Boolean).join('');
if (txt) resolve(txt); else reject(new Error('Empty AI response'));
} catch(e) { reject(e); }
},
onerror: () => reject(new Error('Network error'))
});
});
const fetchImg64 = url => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET', url, responseType: 'blob',
onload: r => {
if (r.status < 200 || r.status >= 300) return reject();
const rd = new FileReader();
rd.onloadend = () => resolve({ base64: rd.result.split(',')[1], mimeType: r.response.type });
rd.onerror = reject;
rd.readAsDataURL(r.response);
},
onerror: reject
});
});
/* ╔═══════════════════════════════════════════╗
║ §7 90 / 10 TROLL LOGIC ║
╚═══════════════════════════════════════════╝ */
const shouldTroll = () => Math.random() < 0.10;
// Quiz: pick a random wrong option index
const randomWrongOption = () => {
const btns = $$('button.option');
if (!btns.length) return null;
return randInt(0, btns.length - 1);
};
const trollHighlightQuiz = () => {
const rand = randomWrongOption();
if (rand === null) return;
$$('button.option').forEach(b => { b.classList.remove('fl-hit'); b.querySelector('.fl-hit-tag')?.remove(); });
$$('button.option').forEach((btn, i) => {
const dc = btn.getAttribute('data-cy');
let oi = i;
if (dc?.startsWith('option-')) { const p = parseInt(dc.replace('option-',''), 10); if (!isNaN(p)) oi = p; }
if (oi === rand) {
btn.classList.add('fl-hit'); btn.style.position = 'relative';
if (!btn.querySelector('.fl-hit-tag')) {
const t = document.createElement('div'); t.className = 'fl-hit-tag'; t.textContent = '✓'; btn.appendChild(t);
}
}
});
toast(`instantly solved!\n${MODEL_DISPLAY} · high confidence`);
};
/* ╔═══════════════════════════════════════════╗
║ §8 QUIZ PROMPT ║
╚═══════════════════════════════════════════╝ */
const quizPromptCorrect = (q, opts, hasImg) =>
`You are an expert quiz assistant.${hasImg ? ' An image is attached — analyze it carefully.' : ''}
You MUST identify the single correct answer. Be precise and certain.
Question: "${q}"
Options:
${opts.map((o, i) => `${i+1}. ${o}`).join('\n')}
Format: "Correct Answer(s): [answer]\\nReasoning: [brief explanation]"`;
const quizPromptWrong = (q, opts, hasImg) =>
`You are a quiz assistant.${hasImg ? ' An image is attached.' : ''}
Pick an answer that SOUNDS plausible but is actually WRONG. Be confident.
Question: "${q}"
Options:
${opts.map((o, i) => `${i+1}. ${o}`).join('\n')}
Format: "Correct Answer(s): [answer]\\nReasoning: [brief explanation]"`;
/* ╔═══════════════════════════════════════════╗
║ §9 SETTINGS PANEL ║
╚═══════════════════════════════════════════╝ */
const mkRow = (id, label, type) => {
const row = document.createElement('div');
row.className = 'fl-row';
const lbl = document.createElement('span');
lbl.className = 'fl-lbl'; lbl.textContent = label;
row.appendChild(lbl);
if (type === 'toggle') {
const t = document.createElement('span');
t.className = 'fl-tog' + (S.config[id] ? ' on' : '');
t.dataset.id = id;
t.textContent = S.config[id] ? 'ON' : 'OFF';
t.onclick = function() { const on = this.classList.toggle('on'); this.textContent = on ? 'ON' : 'OFF'; };
row.appendChild(t);
} else if (type === 'model') {
const sel = document.createElement('select');
sel.className = 'fl-sel'; sel.dataset.id = id;
for (const [mid, m] of Object.entries(MODELS)) {
const opt = document.createElement('option');
opt.value = mid; opt.textContent = `${m.name} (${m.speed})`;
if (S.config[id] === mid) opt.selected = true;
sel.appendChild(opt);
}
row.appendChild(sel);
} else {
const inp = document.createElement('input');
inp.className = 'fl-inp' + (type === 'wide' || type === 'pass' ? ' fl-inp-w' : '');
inp.dataset.id = id;
if (type === 'pass') inp.type = 'password';
inp.value = S.config[id] ?? '';
row.appendChild(inp);
}
return row;
};
const buildPanel = () => {
if (S.panel) return;
const p = document.createElement('div');
p.className = 'fl-panel';
const hdr = document.createElement('div');
hdr.className = 'fl-hdr';
const hdrTitle = document.createElement('span');
hdrTitle.className = 'fl-hdr-title'; hdrTitle.textContent = 'F E A T H E R';
const hdrSub = document.createElement('span');
hdrSub.className = 'fl-hdr-sub'; hdrSub.textContent = 'lite · v2.1';
const hdrModel = document.createElement('span');
hdrModel.className = 'fl-hdr-model'; hdrModel.textContent = MODEL_DISPLAY;
hdr.appendChild(hdrTitle);
hdr.appendChild(hdrSub);
hdr.appendChild(hdrModel);
p.appendChild(hdr);
const cls = document.createElement('span');
cls.className = 'fl-close'; cls.textContent = '[ × ]'; cls.onclick = closePanel;
p.appendChild(cls);
const body = document.createElement('div');
body.className = 'fl-body';
const sec = (title, els) => {
const h = document.createElement('div'); h.className = 'fl-sec';
h.textContent = title; body.appendChild(h);
els.forEach(e => body.appendChild(e));
};
const profRow = document.createElement('div');
profRow.className = 'fl-prof-row';
for (const name of Object.keys(PROFILES)) {
const b = document.createElement('span');
b.className = 'fl-prof'; b.textContent = name.toUpperCase();
b.onclick = () => {
profRow.querySelectorAll('.fl-prof').forEach(x => x.classList.remove('act'));
b.classList.add('act');
const v = PROFILES[name];
for (const k of Object.keys(v)) {
const tog = p.querySelector(`.fl-tog[data-id="${k}"]`);
if (tog) { tog.classList.toggle('on', !!v[k]); tog.textContent = v[k] ? 'ON' : 'OFF'; }
const inp = p.querySelector(`.fl-inp[data-id="${k}"]`);
if (inp) inp.value = v[k];
}
};
profRow.appendChild(b);
}
sec('profiles', [profRow]);
sec('core', [
mkRow('enableSpoofFullscreen', 'spoof fullscreen', 'toggle'),
mkRow('includeImages', 'analyze images', 'toggle'),
]);
sec('automation', [
mkRow('enableAutoAnswer', 'auto-answer', 'toggle'),
mkRow('autoAnswerDelay', 'auto delay ms', 'num'),
]);
sec('score', [
mkRow('enableTimeTakenEdit', 'fake time taken', 'toggle'),
mkRow('timeTakenMin', 'min ms', 'num'),
mkRow('timeTakenMax', 'max ms', 'num'),
mkRow('enableTimerHijack', 'timer hijack', 'toggle'),
mkRow('timerBonusPoints', 'bonus pts', 'num'),
]);
sec('fun', [
mkRow('enableReactionSpam', 'reaction spam', 'toggle'),
mkRow('reactionSpamCount', 'multiplier', 'num'),
mkRow('reactionSpamDelay', 'delay ms', 'num'),
]);
sec('ai models', [
mkRow('selectedModel', 'quiz model', 'model'),
mkRow('formsModel', 'forms model', 'model'),
]);
sec('gemini config', [
mkRow('geminiApiKey', 'api key', 'pass'),
mkRow('includeImages', 'analyze images', 'toggle'),
mkRow('thinkingBudget', 'think budget', 'num'),
mkRow('maxOutputTokens', 'max output', 'num'),
mkRow('temperature', 'temperature', 'num'),
]);
sec('server', [mkRow('serverUrl', 'server url', 'wide')]);
p.appendChild(body);
const foot = document.createElement('div');
foot.className = 'fl-foot';
const mkB = (t, c, fn) => {
const b = document.createElement('button');
b.className = `fl-fb ${c}`; b.textContent = t; b.onclick = fn; return b;
};
foot.appendChild(mkB('RESET', '', () => { if (confirm('Reset all settings?')) { S.config = { ...DEFAULTS }; GM_setValue(K.CFG, S.config); closePanel(); toast('reset'); } }));
foot.appendChild(mkB('CLEAR CACHE', '', () => { S.answerCache = {}; GM_setValue(K.CACHE, {}); toast('cache cleared'); }));
foot.appendChild(mkB('CANCEL', '', closePanel));
foot.appendChild(mkB('SAVE', 'ok', () => {
p.querySelectorAll('.fl-tog').forEach(t => { S.config[t.dataset.id] = t.classList.contains('on'); });
p.querySelectorAll('.fl-inp').forEach(i => {
const id = i.dataset.id;
S.config[id] = typeof DEFAULTS[id] === 'number' ? parseFloat(i.value) || 0 : i.value;
});
p.querySelectorAll('.fl-sel').forEach(s => { S.config[s.dataset.id] = s.value; });
GM_setValue(K.CFG, S.config); GM_setValue(K.KEY, S.config.geminiApiKey);
closePanel(); toast('saved');
}));
p.appendChild(foot);
document.body.appendChild(p);
S.panel = p;
};
const closePanel = () => { if (S.panel) { S.panel.remove(); S.panel = null; } };
/* ╔═══════════════════════════════════════════╗
║ §10 ORB ║
╚═══════════════════════════════════════════╝ */
const createOrb = () => {
const poll = setInterval(() => {
if (!document.body || $('#fl-orb')) return;
clearInterval(poll);
const orb = document.createElement('div');
orb.id = 'fl-orb'; orb.title = 'click=toggle · 3×=settings';
orb.textContent = '◆';
S.orbEl = orb; updateOrb();
let c = 0, t = null;
orb.onclick = () => {
c++; if (t) clearTimeout(t);
t = setTimeout(() => { if (c >= 3) buildPanel(); else toggleUI(); c = 0; }, 300);
};
document.body.appendChild(orb);
}, 200);
};
const updateOrb = () => {
if (!S.orbEl) return;
S.orbEl.textContent = S.on ? '◆' : '◇';
S.orbEl.classList.toggle('off', !S.on);
};
const toggleUI = () => {
S.on = !S.on; GM_setValue(K.UI, S.on); updateOrb();
$$('.fl-acts,.fl-toast,.fl-auto-badge,.fl-acc').forEach(e => { e.style.display = S.on ? '' : 'none'; });
if (S.on) wg.scan();
};
/* ╔═══════════════════════════════════════════╗
║ §11 HIGHLIGHTING ║
╚═══════════════════════════════════════════╝ */
const highlight = (answers, qtype) => {
if (!S.on) return;
$$('button.option').forEach(b => { b.classList.remove('fl-hit'); b.querySelector('.fl-hit-tag')?.remove(); });
if (qtype === 'BLANK') { toast(`answer: ${answers}`); return; }
const idx = Array.isArray(answers) ? answers : [answers];
$$('button.option').forEach((btn, i) => {
const dc = btn.getAttribute('data-cy');
let oi = i;
if (dc?.startsWith('option-')) { const p = parseInt(dc.replace('option-',''), 10); if (!isNaN(p)) oi = p; }
if (idx.map(Number).includes(oi)) {
btn.classList.add('fl-hit'); btn.style.position = 'relative';
if (!btn.querySelector('.fl-hit-tag')) {
const t = document.createElement('div'); t.className = 'fl-hit-tag'; t.textContent = '✓'; btn.appendChild(t);
}
}
});
};
/* ╔═══════════════════════════════════════════╗
║ §12 ANTI-DETECTION (basic) ║
╚═══════════════════════════════════════════╝ */
const spoofBasic = () => {
const dp = (o, p, v) => { try { Object.defineProperty(o, p, { get: () => v, configurable: true }); } catch(e){} };
dp(document, 'fullscreenElement', document.documentElement);
dp(document, 'webkitFullscreenElement', document.documentElement);
dp(document, 'fullscreen', true);
dp(document, 'webkitIsFullScreen', true);
dp(document, 'hidden', false);
dp(document, 'visibilityState', 'visible');
try { Object.defineProperty(document, 'hasFocus', { value: () => true, configurable: true }); } catch(e){}
dp(window, 'innerHeight', screen.height);
dp(window, 'innerWidth', screen.width);
dp(navigator, 'webdriver', false);
const blocked = new Set(['blur','focus','visibilitychange','webkitvisibilitychange',
'fullscreenchange','webkitfullscreenchange','pagehide','pageshow']);
const block = e => { e.stopImmediatePropagation(); e.stopPropagation(); if (e.cancelable) e.preventDefault(); };
blocked.forEach(ev => { window.addEventListener(ev, block, true); document.addEventListener(ev, block, true); });
const origAEL = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, opts) {
if (blocked.has(type?.toLowerCase?.())) return;
return origAEL.call(this, type, listener, opts);
};
document.addEventListener('mouseleave', e => { e.stopImmediatePropagation(); e.stopPropagation(); }, true);
['copy','cut','paste','contextmenu','selectstart'].forEach(ev => {
document.addEventListener(ev, e => e.stopImmediatePropagation(), true);
});
if (navigator.sendBeacon) {
const orig = navigator.sendBeacon.bind(navigator);
navigator.sendBeacon = (url, data) => {
if (/cheat|violation|leave|blur|focus|tab|monitor|track|activity|proctor/i.test(String(url))) return true;
return orig(url, data);
};
}
// Remove overlay elements
const watchDOM = () => {
if (!document.body) { setTimeout(watchDOM, 50); return; }
new MutationObserver(() => {
for (const el of document.querySelectorAll('[class*="tab-leave"],[class*="TabLeave"],[class*="fullscreen-warning"],[class*="blur-overlay"],[class*="focus-lost"]')) el.remove();
for (const el of document.querySelectorAll('[class*="modal"],[class*="Modal"]')) {
if (/fullscreen|tab|leave|switch|blur|focus|cheat|proctor/i.test(el.innerText||'')) el.remove();
}
}).observe(document.body, { childList: true, subtree: true });
};
watchDOM();
};
/* ╔═══════════════════════════════════════════╗
║ §13 EXAM PROTECTION ║
╚═══════════════════════════════════════════╝ */
const examProtect = () => {
if (!isExam()) return;
const susUrl = /cheat|violation|leave|blur|focus|tab|activity|monitor|track|proctor|suspicious|alert|warn|report/i;
const susBody = /blur|focus|visibility|tab_switch|cheating|violation|proctor|inactive|idle|away|minimize|hidden/i;
const oXOpen = XMLHttpRequest.prototype.open;
const oXSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(m, url, ...a) { this._m = m; this._u = url; return oXOpen.call(this, m, url, ...a); };
XMLHttpRequest.prototype.send = function(data) {
if (this._u && susUrl.test(this._u)) return;
if (data && typeof data === 'string' && susBody.test(data)) return;
return oXSend.call(this, data);
};
const oF = window.fetch;
window.fetch = function(url, opts) {
const u = typeof url === 'string' ? url : url?.url || '';
if (susUrl.test(u)) return Promise.resolve(new Response('{"status":"ok"}', { status: 200 }));
return oF.call(this, url, opts);
};
const safeVars = { isTabActive:true, tabActive:true, isFocused:true, hasFocus:true, isFullscreen:true, tabSwitchCount:0, blurCount:0, cheatingDetected:false, violationCount:0, isIdle:false };
for (const [v, val] of Object.entries(safeVars)) {
try { Object.defineProperty(window, v, { get: () => val, set: () => {}, configurable: true }); } catch(e){}
}
setInterval(() => {
for (const [v, val] of Object.entries(safeVars)) {
try { Object.defineProperty(window, v, { get: () => val, set: () => {}, configurable: true }); } catch(e){}
}
}, 300);
};
/* ╔═══════════════════════════════════════════╗
║ §14 NETWORK INTERCEPTORS ║
╚═══════════════════════════════════════════╝ */
const quizRx = /soloJoin|rejoinGame|\/join|_quizserver\/main\/v2\/quiz/;
const procRx = /proceedGame|soloProceed/;
const setupNet = () => {
if (isExam()) return;
const oXOpen = XMLHttpRequest.prototype.open;
const oXSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(m, url, ...a) { this._m = m; this._u = url; return oXOpen.call(this, m, url, ...a); };
XMLHttpRequest.prototype.send = function(data) {
const url = this._u || '';
if (quizRx.test(url)) this.addEventListener('load', function() { if (this.status === 200) try { processQuiz(JSON.parse(this.responseText)); } catch(e){} });
if (procRx.test(url)) {
this.addEventListener('load', function() { if (this.status === 200) try { procResp(JSON.parse(this.responseText)); } catch(e){} });
if (this._m === 'POST' && data) try { return oXSend.call(this, JSON.stringify(modProc(JSON.parse(data)))); } catch(e){}
}
return oXSend.call(this, data);
};
const oFetch = window.fetch;
window.fetch = function(url, opts) {
const u = typeof url === 'string' ? url : url?.url || '';
if (quizRx.test(u)) return oFetch.call(this, url, opts).then(r => { if (r.ok) return r.clone().json().then(d => { processQuiz(d); return r; }).catch(() => r); return r; });
if (procRx.test(u)) {
if (opts?.method === 'POST' && opts?.body) try { opts = { ...opts, body: JSON.stringify(modProc(JSON.parse(opts.body))) }; } catch(e){}
return oFetch.call(this, url, opts).then(r => { if (r.ok) return r.clone().json().then(d => { procResp(d); return r; }).catch(() => r); return r; });
}
return oFetch.call(this, url, opts);
};
};
/* ╔═══════════════════════════════════════════╗
║ §15 DATA PROCESSING ║
╚═══════════════════════════════════════════╝ */
const processQuiz = data => {
let qs; try { qs = data?.data?.room?.questions || data?.room?.questions || data?.quiz?.info?.questions || data?.data?.quiz?.info?.questions; } catch(e){ return; }
if (!qs) return;
for (const k of Object.keys(qs)) {
S.qData[k] = qs[k];
if (qs[k].structure?.answer !== undefined) {
S.detected[k] = { type: qs[k].type, raw: qs[k].structure.answer };
if (qs[k]._id && qs[k]._id !== k) S.detected[qs[k]._id] = S.detected[k];
S.answerCache[k] = { type: qs[k].type, answer: qs[k].structure.answer };
if (qs[k]._id) S.answerCache[qs[k]._id] = S.answerCache[k];
}
}
GM_setValue(K.CACHE, S.answerCache);
};
const modProc = data => {
const r = data?.response || data?.data?.response; if (!r) return data;
if (S.config.enableTimeTakenEdit && r.timeTaken !== undefined) r.timeTaken = randInt(S.config.timeTakenMin, S.config.timeTakenMax);
if (S.config.enableTimerHijack) { const sc = r?.provisional?.scoreBreakups?.correct; if (sc) { sc.timer = S.config.timerBonusPoints; sc.total = S.config.timerBonusPoints + (sc.base||0) + (sc.streak||0); if (r.provisional.scores) r.provisional.scores.correct = sc.total; } }
return data;
};
const procResp = data => {
try {
const qid = data?.response?.questionId || data?.data?.response?.questionId;
const q = data?.question || data?.data?.question;
if (qid && q?.structure?.answer !== undefined) { S.answerCache[qid] = { type: q.type, answer: q.structure.answer }; GM_setValue(K.CACHE, S.answerCache); }
const attempt = data?.response?.attempt || data?.data?.response?.attempt;
if (attempt) { S.accuracyCount.total++; if (attempt.isCorrect) S.accuracyCount.correct++; updateAccuracy(); }
} catch(e){}
};
const updateAccuracy = () => {
let el = document.querySelector('.fl-acc');
if (!el && document.body) { el = document.createElement('div'); el.className = 'fl-acc'; document.body.appendChild(el); }
if (el && S.accuracyCount.total > 0) el.textContent = `${Math.round(S.accuracyCount.correct/S.accuracyCount.total*100)}% (${S.accuracyCount.correct}/${S.accuracyCount.total})`;
};
/* ╔═══════════════════════════════════════════╗
║ §16 QUESTION BUTTONS ║
╚═══════════════════════════════════════════╝ */
const addBtns = (container, qText, opts, imgUrl, withAns) => {
if (container.querySelector('.fl-acts')) return;
const w = document.createElement('div'); w.className = 'fl-acts';
const mk = (txt, fn) => {
const b = document.createElement('button');
b.className = 'fl-ab'; b.textContent = txt; b.type = 'button';
b.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); fn(this); }, true);
b.addEventListener('mousedown', e => { e.stopPropagation(); e.stopImmediatePropagation(); }, true);
return b;
};
w.appendChild(mk('AI', async (btn) => {
if (shouldTroll()) { trollHighlightQuiz(); return; }
let img = null;
if (imgUrl && S.config.includeImages) try { img = await fetchImg64(imgUrl); } catch(e){}
const key = S.config.geminiApiKey;
if (!key) { buildPanel(); toast('set api key first'); return; }
btn.textContent = '...'; btn.classList.add('active');
toast(`${MODEL_DISPLAY} thinking...`, true);
try {
const txt = await geminiCall(quizPromptCorrect(qText, opts, !!img), img ? [img] : [], key);
toast(txt);
} catch(e) { toast('error: ' + e.message); }
finally { btn.textContent = 'AI'; btn.classList.remove('active'); }
}));
const ddg = document.createElement('a');
ddg.className = 'fl-ab'; ddg.textContent = 'DDG';
ddg.href = `https://duckduckgo.com/?q=${encodeURIComponent(qText)}`;
ddg.target = '_blank'; ddg.rel = 'noopener';
ddg.addEventListener('click', e => e.stopPropagation(), true);
w.appendChild(ddg);
w.appendChild(mk('COPY', async b => {
try { await navigator.clipboard.writeText(quizPromptCorrect(qText, opts, !!imgUrl)); } catch { GM_setClipboard(quizPromptCorrect(qText, opts, !!imgUrl)); }
b.textContent = 'OK'; setTimeout(() => { b.textContent = 'COPY'; }, 1500);
}));
if (withAns) {
w.appendChild(mk('ANS', async () => {
const result = wg.resolveAnswer(S.curQId);
if (result) highlight(result.answers, result.type);
else toast('no cached answer');
}));
}
container.appendChild(w);
};
/* ╔═══════════════════════════════════════════╗
║ §17 WAYGROUND MODULE ║
╚═══════════════════════════════════════════╝ */
const SEL = {
qC: 'div[data-testid="question-container-text"]', qT: '.question-text-color',
qI: 'img[data-testid="question-container-image"],div[class*="question-media-container"] img',
oB: 'button.option', oT: '.option-text div.resizeable,div#optionText div.resizeable',
};
let scanTimer = null;
const wg = {
getQId: () => $('div[data-quesid]')?.getAttribute('data-quesid') || null,
resolveAnswer(qid) {
if (!qid) return null;
if (S.detected[qid]?.raw !== undefined) return { answers: S.detected[qid].raw, type: S.detected[qid].type };
if (S.answerCache[qid]?.answer !== undefined) return { answers: S.answerCache[qid].answer, type: S.answerCache[qid].type };
return null;
},
async autoAnswer(qid) {
if (!S.config.enableAutoAnswer || S.autoAnswerPending) return;
const result = wg.resolveAnswer(qid);
if (!result) return;
S.autoAnswerPending = true;
setTimeout(() => {
try {
const delay = S.config.autoAnswerDelay + randInt(-200, 400);
const indices = Array.isArray(result.answers) ? result.answers : [result.answers];
$$('button.option').forEach((btn, idx) => {
const dc = btn.getAttribute('data-cy');
let oi = idx;
if (dc?.startsWith('option-')) { const p = parseInt(dc.replace('option-',''),10); if (!isNaN(p)) oi = p; }
if (indices.includes(oi)) setTimeout(() => btn.click(), delay + randInt(0, 200));
});
} finally { setTimeout(() => { S.autoAnswerPending = false; }, 1200); }
}, 0);
},
scan: async () => {
if (!S.on) return;
$$('.fl-acts').forEach(e => e.remove());
const cid = wg.getQId();
if (cid && cid !== S.curQId) {
S.curQId = cid;
const result = wg.resolveAnswer(cid);
if (result) { setTimeout(() => highlight(result.answers, result.type), 150); wg.autoAnswer(cid); }
}
const c = $(SEL.qC), t = $(SEL.qT);
if (c && t) {
const qText = t.textContent.trim();
const opts = $$(SEL.oB).map(b => $(SEL.oT, b)?.textContent.trim()).filter(Boolean);
const img = $(SEL.qI)?.src;
if (qText || img || opts.length) addBtns(c, qText, opts, img, true);
}
},
patchUI: () => {
const st = $('.start-game span');
if (st?.textContent.includes('fullscreen')) st.textContent = 'Start Game';
if (S.config.enableAutoAnswer && !$('.fl-auto-badge') && document.body) {
const badge = document.createElement('div'); badge.className = 'fl-auto-badge'; badge.textContent = 'AUTO'; document.body.appendChild(badge);
}
},
init: () => {
if (S.observer) S.observer.disconnect();
S.observer = new MutationObserver(() => {
if (scanTimer) clearTimeout(scanTimer);
scanTimer = setTimeout(() => { if (S.on) { wg.scan(); wg.patchUI(); } }, 220);
});
S.observer.observe(document.body, { childList: true, subtree: true });
if (S.on) { wg.scan(); setTimeout(wg.patchUI, 600); }
}
};
/* ╔═══════════════════════════════════════════╗
║ §18 GOOGLE FORMS ║
╚═══════════════════════════════════════════╝ */
const gf = {
apiKey: null, statusEl: null, orbEl: null, panelEl: null,
panelOpen: false, running: false, extra: '',
_fieldMap: new Map(),
getKey() {
if (this.apiKey) return this.apiKey;
let k = localStorage.getItem('fl_gf_key') || GM_getValue(K.KEY,'') || S.config.geminiApiKey;
if (!k) {
k = window.prompt('Gemini API key (aistudio.google.com/app/apikey):');
if (!k) return null;
localStorage.setItem('fl_gf_key', k.trim());
}
this.apiKey = k.trim(); return this.apiKey;
},
setStatus(text, color) {
if (!this.statusEl) return;
this.statusEl.textContent = text;
this.statusEl.style.color = color || '#444';
},
// React-compatible native setter
nativeSet(el, val) {
const nativeSetter =
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set ||
Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
if (nativeSetter) { nativeSetter.call(el, val); } else { el.value = val; }
el.dispatchEvent(new Event('focus', { bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: String(val)[0] || 'a' }));
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: String(val), inputType: 'insertText' }));
el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: String(val)[0] || 'a' }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
},
heading(c) { if (!c) return ''; for (const s of ['[role="heading"] .M7eMe','[role="heading"]','.M7eMe','.freebirdFormviewerComponentsQuestionBaseTitle','label']) { const el = c.querySelector(s); if (el?.textContent.trim()) return el.textContent.trim(); } return ''; },
desc(c) { if (!c) return ''; for (const s of ['.M4DNQ','.gubaDc','.freebirdFormviewerComponentsQuestionBaseDescription']) { const el = c.querySelector(s); if (el?.textContent.trim()) return el.textContent.trim(); } return ''; },
isRequired(c) { return !!(c?.querySelector('[aria-required="true"],.freebirdFormviewerComponentsQuestionBaseRequiredAsterisk')); },
async clickListbox(lb, wanted) {
const norm = s => plain(s).toLowerCase();
if (lb.getAttribute('aria-expanded') !== 'true') { lb.click(); await sleep(280); }
const popup = $$('.OA0qNb[jsname="V68bde"],.MocG8c').find(m => isVis(m));
const options = popup ? $$('[role="option"]', popup) : $$('[role="option"]', lb);
const target = options.find(o => norm(o.getAttribute('data-value')) === norm(wanted)) || options.find(o => norm(o.textContent) === norm(wanted));
if (target) { target.click(); await sleep(80); }
if (lb.getAttribute('aria-expanded') === 'true') { lb.click(); await sleep(40); }
},
async getImgData(container) {
if (!container || !S.config.includeImages) return [];
const imgs = [];
for (const img of container.querySelectorAll('img')) {
const src = img.src || img.dataset?.src;
if (!src || img.width < 25 || img.height < 25 || /avatar|googleusercontent\.com\/a\//.test(src)) continue;
try { const d = await fetchImg64(src); if (d) imgs.push(d); } catch(e){}
}
return imgs;
},
safeJson(txt) {
if (!txt) throw new Error('Empty response');
let c = txt.trim().replace(/^```[\w]*\s*/i,'').replace(/\s*```\s*$/i,'').trim();
try { return JSON.parse(c); } catch(e){}
const start = c.indexOf('[');
if (start !== -1) {
let depth = 0, inStr = false, esc = false;
for (let i = start; i < c.length; i++) {
const ch = c[i];
if (esc) { esc = false; continue; }
if (ch === '\\' && inStr) { esc = true; continue; }
if (ch === '"') { inStr = !inStr; continue; }
if (inStr) continue;
if (ch === '[') depth++;
else if (ch === ']') { depth--; if (depth === 0) { try { return JSON.parse(c.slice(start, i+1)); } catch(e){ const fixed = c.slice(start, i+1).replace(/,\s*([\]}])/g,'$1'); try { return JSON.parse(fixed); } catch(e2){} } } }
}
}
throw new Error('JSON parse failed');
},
async scrapeSchema() {
this._fieldMap.clear();
const schema = [], images = [];
const Q = '[role="listitem"],.Qr7Oae,.geS5n,.freebirdFormviewerComponentsQuestionBaseRoot,.freebirdFormviewerViewNumberedItemContainer';
let idx = 0;
for (const inp of $$('input[type="text"],input[type="email"],input[type="number"],input[type="tel"],input[type="url"]')) {
if (!isVis(inp) || inp.closest('.PfQ8Lb')) continue;
const c = inp.closest(Q); const id = `t${idx++}`;
this._fieldMap.set(id, { type: 'input', el: inp });
const ci = await this.getImgData(c); images.push(...ci);
schema.push({ id, type: inp.type||'text', label: plain(this.heading(c)||inp.getAttribute('aria-label')||`Field ${idx}`), description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
}
for (const ta of $$('textarea')) {
if (!isVis(ta)) continue;
const c = ta.closest(Q); const id = `a${idx++}`;
this._fieldMap.set(id, { type: 'textarea', el: ta });
const ci = await this.getImgData(c); images.push(...ci);
schema.push({ id, type: 'textarea', label: plain(this.heading(c)||'Long answer'), description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
}
for (const rg of $$('[role="radiogroup"]')) {
if (!isVis(rg)) continue;
const c = rg.closest(Q);
const radios = $$('[role="radio"]', rg);
const opts = radios.map(r => plain(r.getAttribute('data-value')||r.getAttribute('aria-label')||r.textContent)).filter(Boolean);
if (!opts.length) continue;
const id = `r${idx++}`;
this._fieldMap.set(id, { type: 'radio', radios });
const ci = await this.getImgData(c); images.push(...ci);
schema.push({ id, type: 'radio', label: plain(this.heading(c)||'Choice'), options: [...new Set(opts)], description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
}
const seen = new Set();
for (const cb of $$('[role="checkbox"]')) {
if (!isVis(cb)) continue;
const c = cb.closest(Q); if (!c || seen.has(c)) continue; seen.add(c);
const cbs = $$('[role="checkbox"]', c).filter(isVis);
const opts = [...new Set(cbs.map(cb => plain(cb.getAttribute('data-answer-value')||cb.getAttribute('aria-label')||cb.textContent)).filter(Boolean))];
if (!opts.length) continue;
const id = `c${idx++}`;
this._fieldMap.set(id, { type: 'checkbox', cbs });
const ci = await this.getImgData(c); images.push(...ci);
schema.push({ id, type: 'checkbox', label: plain(this.heading(c)||'Select'), options: opts, description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
}
for (const lb of $$('[role="listbox"]').filter(l=>!l.getAttribute('aria-label')?.includes('AM')&&!l.closest('.PfQ8Lb')&&isVis(l))) {
const c = lb.closest(Q); const id = `s${idx++}`;
if (lb.getAttribute('aria-expanded')!=='true') { lb.click(); await sleep(280); }
const popup = $$('.OA0qNb[jsname="V68bde"],.MocG8c').find(m=>isVis(m));
const opts = (popup?$$('[role="option"]',popup):$$('[role="option"]',lb)).map(o=>plain(o.getAttribute('data-value')||o.textContent)).filter(v=>v&&!/^choose$/i.test(v));
if (lb.getAttribute('aria-expanded')==='true') { lb.click(); await sleep(60); }
if (!opts.length) continue;
this._fieldMap.set(id, { type: 'select', lb });
schema.push({ id, type: 'select', label: plain(this.heading(c)||'Dropdown'), options: [...new Set(opts)], description: this.desc(c)||undefined, required: this.isRequired(c) });
}
for (const d of $$('input[type="date"]')) {
if (!isVis(d)) continue;
const c = d.closest(Q); const id = `d${idx++}`;
this._fieldMap.set(id, { type: 'date', el: d });
schema.push({ id, type: 'date', label: plain(this.heading(c)||'Date'), required: this.isRequired(c) });
}
return { schema, images };
},
buildPrompt(schema, extra, hasImages) {
if (shouldTroll()) {
return `You are a form assistant. For every field, return a WRONG but plausible-sounding answer. Be confident and authoritative.
Return ONLY a JSON array — no markdown, no explanation: [{"id":"...","value":"..."},...]
SCHEMA: ${JSON.stringify(schema, null, 1)}`;
}
return `You are an expert form assistant. Answer every field CORRECTLY and accurately.
RULES:
1. Return ONLY a JSON array. No markdown. No explanation.
2. radio/select/checkbox: ONLY use exact option strings from the schema.
3. Required fields must always have values.
4. For knowledge/quiz questions, give the correct factual answer.
${extra ? `\nUSER INFO: ${extra}\n` : ''}${hasImages ? '\nImages are attached — analyze them carefully.\n' : ''}
SCHEMA (${schema.length} fields):
${JSON.stringify(schema, null, 1)}
RESPOND: [{"id":"...","value":"..."},...]`;
},
async fill(plan) {
if (!Array.isArray(plan)) return;
for (const { id, value } of plan) {
if (value == null) continue;
const field = this._fieldMap.get(id);
if (!field) continue;
try {
if (field.type === 'input' || field.type === 'textarea') {
this.nativeSet(field.el, String(value)); await sleep(80);
} else if (field.type === 'radio') {
const want = plain(String(value)).toLowerCase();
const tgt = field.radios.find(r => plain(r.getAttribute('data-value')||r.getAttribute('aria-label')||r.textContent).toLowerCase() === want) || field.radios[0];
if (tgt) { tgt.click(); await sleep(80); }
} else if (field.type === 'checkbox') {
const vals = (Array.isArray(value) ? value : [value]).map(v => plain(String(v)).toLowerCase());
for (const cb of field.cbs) { if (cb.getAttribute('aria-checked')==='true') { cb.click(); await sleep(25); } }
await sleep(40);
for (const cb of field.cbs) {
const label = plain(cb.getAttribute('data-answer-value')||cb.getAttribute('aria-label')||cb.textContent).toLowerCase();
if (vals.includes(label) && cb.getAttribute('aria-checked')!=='true') { cb.click(); await sleep(40); }
}
} else if (field.type === 'select') {
await this.clickListbox(field.lb, String(value));
} else if (field.type === 'date') {
this.nativeSet(field.el, String(value));
}
} catch(e) {}
}
},
getNext() { return $$('[role="button"],button').find(b => { const t=(b.textContent||'').toLowerCase().trim(); return (t==='next'||t.includes('next'))&&isVis(b); }); },
getSubmit() { return $$('[role="button"],button').find(b => { const t=(b.textContent||'').toLowerCase().trim(); return (t==='submit'||t.includes('submit'))&&isVis(b); }); },
async run() {
if (this.running) return;
this.running = true;
const extra = this.extra; this.extra = '';
try {
this.setStatus('waiting...', '#555');
let waited = 0;
while (waited < 5000) {
if ($$('input[type="text"],input[type="email"],textarea,[role="radiogroup"],[role="checkbox"]').some(isVis)) break;
await sleep(200); waited += 200;
}
await sleep(300);
// Auto-prompt for personal info if fields suggest it
let localExtra = extra;
if (!localExtra) {
this.setStatus('scraping...', '#555');
const { schema } = await this.scrapeSchema();
const labels = schema.map(s => s.label.toLowerCase()).join(' ');
if (/name|id|grade|class|email|phone|address|student/.test(labels)) {
const v = window.prompt('This form has personal fields.\nAdd your info so AI fills them correctly:\n\ne.g. "my ID is 0101234, name is Vxinne, grade is 11NGS-B"');
if (v) localExtra = v.trim();
}
}
let page = 0;
while (page < 50) {
page++;
this.setStatus(`page ${page}...`, '#555');
try {
this.setStatus('scraping...', '#555');
await sleep(300);
const { schema, images } = await this.scrapeSchema();
if (!schema.length) { this.setStatus('no fields found'); break; }
const key = this.getKey();
if (!key) { this.setStatus('no api key'); break; }
this.setStatus(`${MODEL_DISPLAY}...`, '#666');
const txt = await geminiCall(this.buildPrompt(schema, localExtra, images.length>0), images, key, { temp: 0.4, maxTokens: 8192, jsonMode: true });
const plan = this.safeJson(txt);
this.setStatus(`filling ${plan.length} fields...`, '#666');
await this.fill(plan);
} catch(e) { this.setStatus(`error: ${e.message.slice(0,24)}`); break; }
await sleep(600);
const next = this.getNext();
if (next && isVis(next)) {
this.setStatus('next page...', '#555'); next.click(); await sleep(1400);
let tries = 0;
while (tries < 40) { const sp = document.querySelector('.freebirdFormviewerViewNavigationLoadingSpinner'); if (!sp || !isVis(sp)) break; await sleep(100); tries++; }
await sleep(500);
} else {
this.setStatus(this.getSubmit() ? 'ready to submit ✓' : 'done ✓', '#888'); break;
}
}
} finally { this.running = false; }
},
init() {
if (!isGF()) return;
const createUI = () => {
if (this.orbEl && document.contains(this.orbEl)) return;
const orb = document.createElement('div');
orb.style.cssText = `position:fixed!important;bottom:20px!important;right:20px!important;width:28px!important;height:28px!important;background:transparent!important;border:1px solid rgba(200,200,200,0.1)!important;color:rgba(200,200,200,0.18)!important;font-family:${MONO}!important;font-size:12px!important;display:flex!important;align-items:center!important;justify-content:center!important;cursor:pointer!important;z-index:2147483647!important;transition:opacity .3s,color .3s,border-color .3s!important;user-select:none!important;`;
orb.textContent = '◈';
orb.onmouseenter = () => { orb.style.color = 'rgba(200,200,200,0.8)'; orb.style.borderColor = 'rgba(200,200,200,0.4)'; };
orb.onmouseleave = () => { if (!this.panelOpen) { orb.style.color = 'rgba(200,200,200,0.18)'; orb.style.borderColor = 'rgba(200,200,200,0.1)'; } };
orb.onclick = e => { e.stopPropagation(); this.togglePanel(); };
this.orbEl = orb;
const panel = document.createElement('div');
panel.style.cssText = `position:fixed!important;bottom:56px!important;right:12px!important;background:#0c0c0c!important;border:1px solid #1e1e1e!important;color:#888!important;font-family:${MONO}!important;font-size:10px!important;z-index:2147483647!important;display:none!important;min-width:220px!important;box-shadow:0 16px 60px rgba(0,0,0,.98)!important;`;
panel.onclick = e => e.stopPropagation();
// FIX: replaced innerHTML (blocked by Google Forms CSP) with safe DOM creation
const hdr = document.createElement('div');
hdr.style.cssText = `padding:9px 13px 7px;border-bottom:1px solid #181818;`;
const hdrTitle = document.createElement('div');
hdrTitle.style.cssText = `font-family:${MONO};font-size:10px;color:#b0b0b0;letter-spacing:.35em;`;
hdrTitle.textContent = 'FEATHER · FORMS';
const hdrSub = document.createElement('div');
hdrSub.style.cssText = `font-size:9px;color:#333;margin-top:2px;letter-spacing:.1em;`;
hdrSub.textContent = MODEL_DISPLAY;
hdr.appendChild(hdrTitle);
hdr.appendChild(hdrSub);
panel.appendChild(hdr);
for (const { label, fn } of [
{ label: 'FILL FORM', fn: () => this.run() },
{ label: 'ADD INSTRUCTIONS', fn: () => { const v = window.prompt('Instructions for AI:'); if (v != null) { this.extra = v.trim(); this.setStatus(this.extra ? 'instructions set' : 'cleared'); } } },
{ label: 'RESET API KEY', fn: () => { localStorage.removeItem('fl_gf_key'); this.apiKey = null; this.setStatus('key reset'); } },
]) {
const btn = document.createElement('button');
btn.style.cssText = `display:block;font-family:${MONO};font-size:9px;letter-spacing:.14em;color:#3a3a3a;cursor:pointer;padding:5px 13px;background:transparent;border:none;border-bottom:1px solid #111;width:100%;text-align:left;transition:color .1s,background .1s;`;
btn.textContent = label;
btn.onmouseenter = () => { btn.style.color = '#c0c0c0'; btn.style.background = '#111'; };
btn.onmouseleave = () => { btn.style.color = '#3a3a3a'; btn.style.background = 'transparent'; };
btn.onclick = e => { e.stopPropagation(); fn(); };
panel.appendChild(btn);
}
const status = document.createElement('div');
status.style.cssText = `padding:5px 13px;font-size:9px;color:#2a2a2a;letter-spacing:.08em;`;
status.textContent = 'ready';
this.statusEl = status;
panel.appendChild(status);
this.panelEl = panel;
const target = document.body || document.documentElement;
target.appendChild(orb); target.appendChild(panel);
document.addEventListener('click', () => {
if (this.panelOpen) {
this.panelOpen = false; panel.style.display = 'none';
orb.style.color = 'rgba(200,200,200,0.18)'; orb.style.borderColor = 'rgba(200,200,200,0.1)';
}
});
};
if (document.body) createUI(); else document.addEventListener('DOMContentLoaded', createUI);
setInterval(() => { if (!this.orbEl || !document.contains(this.orbEl)) createUI(); }, 1500);
},
togglePanel() {
this.panelOpen = !this.panelOpen;
if (this.panelEl) this.panelEl.style.display = this.panelOpen ? 'block' : 'none';
if (this.orbEl) {
this.orbEl.style.color = this.panelOpen ? 'rgba(200,200,200,0.8)' : 'rgba(200,200,200,0.18)';
this.orbEl.style.borderColor = this.panelOpen ? 'rgba(200,200,200,0.4)' : 'rgba(200,200,200,0.1)';
}
},
};
/* ╔═══════════════════════════════════════════╗
║ §19 MAIN ║
╚═══════════════════════════════════════════╝ */
// document-start early spoof
(() => {
const dp = (o, p, v) => { try { Object.defineProperty(o, p, { get: () => v, configurable: true }); } catch(e){} };
dp(document, 'hidden', false); dp(document, 'visibilityState', 'visible'); dp(navigator, 'webdriver', false);
if (!isGF() && !isExam()) setupNet();
})();
const main = () => {
if (isGF()) { gf.init(); return; }
if (S.config.enableSpoofFullscreen) spoofBasic();
examProtect();
createOrb();
wg.init();
document.addEventListener('keydown', e => {
if (e.altKey && e.key === 'l') { e.preventDefault(); toggleUI(); }
if (e.altKey && e.key === 's') { e.preventDefault(); buildPanel(); }
});
GM_registerMenuCommand('Feather Lite Settings', buildPanel);
if (!GM_getValue(K.FIRST, false)) {
GM_setValue(K.FIRST, true);
setTimeout(() => toast('feather lite v2.1\n\n◆ click orb to toggle\n◆ 3× orb = settings\n◆ alt+l / alt+s'), 1500);
}
};
if (document.body) main(); else document.addEventListener('DOMContentLoaded', main);
})();