แหล่งที่มาดิบ
max / K-Bot | Kahoot Answer Viewer & Auto-Answer Cheat Mod Menu (Working 2026)

// ==UserScript==
// @name         K-Bot | Kahoot Answer Viewer & Auto-Answer Cheat Mod Menu (Working 2026) 
// @version      2.1
// @namespace    juanbolsa
// @description  A minimalist Kahoot mod menu. Features a live answer viewer and automated responding.
// @author       juanbolsa
// @match        https://kahoot.it/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kahoot.it
// @license      MIT
// @grant        none
// ==/UserScript==
 
(function() {
    'use strict';
 
    // ─── Config ───────────────────────────────────────────────────────────────────
    const VERSION = '2.1';
    const MULTI_SELECT_STAGGER_MS = 60;
 
    // ─── State ────────────────────────────────────────────────────────────────────
    const state = {
        questions: [],
        numQuestions: 0,
        questionNum: -1,
        lastAnsweredQuestion: -1,
        baseDelay: 0,
        randomDelay: 0,
        autoAnswer: false,
        showAnswers: false,
    };
 
    // Keep track of DOM values to avoid redundant updates
    let lastProcessedQuestionText = "";
 
    // ─── DOM Helpers ──────────────────────────────────────────────────────────────
    function queryBySelector(value, tag = '*') {
        return document.querySelector(`${tag}[data-functional-selector="${value}"]`);
    }
 
    function createElement(tag, { styles = {}, className, text } = {}) {
        const el = document.createElement(tag);
        if (className) el.className = className;
        if (text)      el.textContent = text;
        Object.assign(el.style, styles);
        return el;
    }
 
    // ─── Inject Assets ────────────────────────────────────────────────────────────
    const fontLink = document.createElement('link');
    fontLink.rel  = 'stylesheet';
    fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap';
    document.head.appendChild(fontLink);
 
    const globalStyle = document.createElement('style');
globalStyle.textContent = `
        .kb-ui * {
            box-sizing: border-box;
            font-family: 'Inter', system-ui, -apple-system, sans-serif;
        }
        .kb-ui button, .kb-input, .kb-switch-track::before {
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        }
        .kb-ui {
            /* 85% Opacity Backgrounds */
            --bg: rgba(15, 15, 15, 0.75);
            --surface: rgba(24, 24, 24, 0.75);
            --border: rgba(255, 255, 255, 0.1);
            --accent: #f0f0f0;
            --muted: #888;
            --green: #4caf74;
            --red: #c0392b;
            --text: #d8d8d8;
            --label: #aaa;
 
            /* Frosted Glass Effect */
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 1px rgba(255, 255, 255, 0.1);
        }
        .kb-handle {
            cursor: grab;
            user-select: none;
            background: var(--surface);
            border-bottom: 1px solid var(--border);
        }
        .kb-handle:active { cursor: grabbing; }
 
        .kb-input {
            width: 100% !important;
            background-color: rgba(0, 0, 0, 0.3) !important;
            border: 1px solid var(--border) !important;
            color: #ffffff !important;
            border-radius: 4px;
            font-size: 13px;
            font-weight: 600;
            padding: 5px 8px;
            outline: none;
            transition: all 0.2s ease;
            margin-top: 8px;
        }
        .kb-input:focus { border-color: rgba(255, 255, 255, 0.4) !important; }
        .kb-input.ok  { border-color: var(--green) !important; background: rgba(13, 46, 26, 0.6) !important; }
        .kb-input.err { border-color: var(--red) !important; background: rgba(46, 13, 13, 0.6) !important; }
 
        .kb-slider-wrap  { display: flex; flex-direction: column; gap: 3px; }
        .kb-slider-label { display: flex; justify-content: space-between; margin-bottom: 2px; }
        .kb-slider-label span { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
 
        .kb-range {
            -webkit-appearance: none;
            width: 100%;
            height: 4px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 2px;
            outline: none;
        }
        .kb-range::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 14px;
            height: 14px;
            background: var(--accent);
            border-radius: 50%;
            cursor: pointer;
            box-shadow: 0 0 10px rgba(0,0,0,0.5);
        }
 
        .kb-toggle-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 6px 0;
            border-bottom: 1px solid var(--border);
        }
        .kb-toggle-label { color: var(--text); font-size: 13px; font-weight: 600; }
 
        .kb-switch { position: relative; width: 34px; height: 18px; }
        .kb-switch input { opacity: 0; width: 0; height: 0; }
        .kb-switch-track {
            position: absolute;
            inset: 0;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 20px;
            cursor: pointer;
            transition: 0.3s;
        }
        .kb-switch-track::before {
            content: '';
            position: absolute;
            width: 12px;
            height: 12px;
            left: 3px;
            top: 3px;
            background: #777;
            border-radius: 50%;
            transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
        }
        .kb-switch input:checked + .kb-switch-track { background: rgba(76, 175, 116, 0.3); }
        .kb-switch input:checked + .kb-switch-track::before {
            transform: translateX(16px);
            background: var(--green);
            box-shadow: 0 0 8px var(--green);
        }
 
        .kb-divider { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
        .kb-section-title {
            color: var(--muted);
            font-size: 12px;
            font-weight: 800;
            text-transform: uppercase;
            letter-spacing: 0.1em;
            margin-bottom: 3px;
            text-align: center;
        }
        .kb-stat { color: var(--accent); font-size: 18px; font-weight: 700; text-align: center; margin-top: 15px;}
        .kb-credit { color: #666; font-size: 13px; text-align: center; margin-top: 2px; font-weight: 600; }
    `;
    document.head.appendChild(globalStyle);
 
    // ─── UI Construction ──────────────────────────────────────────────────────────
    const uiElement = createElement('div', {
        className: 'kb-ui',
        styles: { position: 'fixed', top: '5%', left: '5%', width: '240px', background: 'var(--bg)', border: '1px solid var(--border)', borderRadius: '6px', boxShadow: '0 8px 32px rgba(0,0,0,0.7)', zIndex: '9999' }
    });
 
    const handle = createElement('div', {
        className: 'kb-handle',
        styles: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 8px', background: 'var(--surface)', borderBottom: '1px solid var(--border)' }
    });
 
    handle.append(createElement('span', { text: 'K-Bot v'+VERSION, styles: { color: 'var(--accent)', fontSize: '13px', fontWeight: '700' } }));
 
    const titleControls = createElement('div', { styles: { display: 'flex', gap: '5px' } });
    const minimizeButton = createElement('button', { text: '-', styles: { background: '#888', border: 'none', width: '22px', height: '20px', cursor: 'pointer', borderRadius: '3px', color: '#fff', fontWeight: '800',} });
    const closeButton = createElement('button', { text: '✕', styles: { background: '#c0392b', color: '#fff', border: 'none', width: '22px', height: '20px', cursor: 'pointer', borderRadius: '3px', fontWeight: '800',} });
 
    titleControls.append(minimizeButton, closeButton);
    handle.append(titleControls);
    uiElement.appendChild(handle);
 
    const body = createElement('div', { styles: { padding: '15px', display: 'flex', flexDirection: 'column', gap: '6px' } });
    uiElement.appendChild(body);
 
    // Quiz ID Input
    const quizSection = document.createElement('div');
    quizSection.append(createElement('div', { className: 'kb-section-title', text: 'Quiz ID' }));
    const inputBox = createElement('input', { className: 'kb-input' });
    inputBox.placeholder = 'Enter Quiz ID (NOT PIN)';
    quizSection.appendChild(inputBox);
    body.appendChild(quizSection);
    body.appendChild(createElement('hr', { className: 'kb-divider' }));
 
    // Sliders
    function makeSlider(labelText, { min, max, step, value }, onInput) {
        const wrap = createElement('div', { className: 'kb-slider-wrap' });
        const labelRow = createElement('div', { className: 'kb-slider-label' });
        const valSpan = createElement('span', { text: `${value} ms`, styles: { color: 'var(--accent)' } });
        labelRow.append(createElement('span', { text: labelText, styles: { color: 'var(--label)' } }), valSpan);
        const input = createElement('input', { className: 'kb-range' });
        input.type = 'range'; Object.assign(input, { min, max, step, value });
        input.oninput = () => { valSpan.textContent = `${input.value} ms`; onInput(+input.value); };
        wrap.append(labelRow, input);
        return wrap;
    }
 
    body.appendChild(createElement('div', { className: 'kb-section-title', text: 'Delay' }));
    body.appendChild(makeSlider('Base', { min: 0, max: 10000, step: 100, value: state.baseDelay }, v => state.baseDelay = v));
    body.appendChild(makeSlider('Random ±', { min: 0, max: 5000, step: 100, value: state.randomDelay }, v => state.randomDelay = v));
    body.appendChild(createElement('hr', { className: 'kb-divider' }));
 
    // Toggles
    function makeToggle(label, key) {
        const row = createElement('div', { className: 'kb-toggle-row' });
        row.append(createElement('span', { className: 'kb-toggle-label', text: label }));
        const sw = createElement('label', { className: 'kb-switch' });
        const input = document.createElement('input'); input.type = 'checkbox';
        input.onchange = () => state[key] = input.checked;
        sw.append(input, createElement('span', { className: 'kb-switch-track' }));
        row.append(sw);
        return row;
    }
 
    body.appendChild(createElement('div', { className: 'kb-section-title', text: 'Answering' }));
    body.appendChild(makeToggle('Auto Answer', 'autoAnswer'));
    body.appendChild(makeToggle('Show Answers', 'showAnswers'));
 
    const questionsLabel = createElement('div', { className: 'kb-stat', text: 'Question 0 / 0' });
    body.appendChild(questionsLabel);
    body.appendChild(createElement('div', { className: 'kb-credit', text: `v${VERSION} · juanbolsa` }));
    document.body.appendChild(uiElement);
 
    // ─── Logic ────────────────────────────────────────────────────────────────────
 
    function triggerReactClick(btn) {
        const fiberKey = Object.keys(btn).find((k) => k.startsWith('__reactFiber'));
        if (!fiberKey) return;
 
        let fiber = btn[fiberKey];
        while (fiber) {
            const onClick = fiber.memoizedProps?.onClick;
            if (onClick) {
                const nativeEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
                const trusted = new Proxy(nativeEvent, {
                    get(target, prop) {
                        if (prop === 'isTrusted') return true;
                        const val = target[prop];
                        return typeof val === 'function' ? val.bind(target) : val;
                    },
                });
                onClick(trusted);
                return;
            }
            fiber = fiber.return;
        }
    }
 
    function answer(question, delay) {
        const performClick = (idx) => {
            const btn = queryBySelector(`answer-${idx}`, 'button');
            if (btn) triggerReactClick(btn);
        };
 
        if (question.type === 'quiz') {
            setTimeout(() => performClick(question.answers[0]), delay);
        } else if (question.type === 'multiple_select_quiz') {
            question.answers.forEach((ans, i) => {
                setTimeout(() => performClick(ans), delay + (i * MULTI_SELECT_STAGGER_MS));
            });
            setTimeout(() => {
                const submit = queryBySelector('multi-select-submit-button', 'button');
                if (submit) triggerReactClick(submit);
            }, delay + (question.answers.length * MULTI_SELECT_STAGGER_MS));
        }
    }
 
    function highlightAnswers(question) {
        question.answers.forEach(i => {
            const btn = queryBySelector(`answer-${i}`, 'button');
            if (btn) btn.style.backgroundColor = 'rgb(0,255,0)';
        });
        question.incorrectAnswers?.forEach(i => {
            const btn = queryBySelector(`answer-${i}`, 'button');
            if (btn) btn.style.backgroundColor = 'rgb(255,0,0)';
        });
    }
 
    function parseQuestions(raw) {
        return raw.map(q => {
            const p = { type: q.type, time: q.time, answers: [] };
            if (q.choices) {
                p.incorrectAnswers = [];
                q.choices.forEach((c, i) => (c.correct ? p.answers : p.incorrectAnswers).push(i));
            }
            return p;
        });
    }
 
    // ─── The Main Loop (Optimized) ────────────────────────────────────────────────
    function pollFrame() {
        const counterEl = queryBySelector('question-index-counter', 'div');
        const currentText = counterEl ? counterEl.textContent : "";
 
        // DIRTY CHECK: Only run logic if the question number text actually changed
        if (currentText !== lastProcessedQuestionText) {
            lastProcessedQuestionText = currentText;
 
            if (currentText) {
                state.questionNum = parseInt(currentText) - 1;
                questionsLabel.textContent = `Question ${state.questionNum + 1} / ${state.numQuestions}`;
            }
        }
 
        // Check if buttons are ready and we haven't answered this specific question index yet
        const firstBtn = queryBySelector('answer-0', 'button');
        if (firstBtn && state.lastAnsweredQuestion !== state.questionNum) {
            state.lastAnsweredQuestion = state.questionNum;
            const qData = state.questions[state.questionNum];
            if (qData) {
                if (state.showAnswers) highlightAnswers(qData);
                if (state.autoAnswer) answer(qData, state.baseDelay + Math.floor(Math.random() * state.randomDelay));
            }
        }
 
        requestAnimationFrame(pollFrame);
    }
 
    // ─── Event Handlers ──────────────────────────────────────────────────────────
    inputBox.oninput = async () => {
        const id = inputBox.value.trim();
        inputBox.className = 'kb-input';
        if (!id) return;
        try {
            const res = await fetch(`https://kahoot.it/rest/kahoots/${id}`);
            if (!res.ok) throw 1;
            const data = await res.json();
            state.questions = parseQuestions(data.questions);
            state.numQuestions = state.questions.length;
            inputBox.classList.add('ok');
            questionsLabel.textContent = `Question 1 / ${state.numQuestions}`;
        } catch(e) {
            inputBox.classList.add('err');
        }
    };
 
    minimizeButton.onclick = () => {
        const isMin = body.style.display === 'none';
        body.style.display = isMin ? 'flex' : 'none';
        minimizeButton.textContent = isMin ? '-' : '+';
    };
 
    closeButton.onclick = () => uiElement.remove();
 
    // Draggable Logic
    let dragging = false, offset = [0,0];
    handle.addEventListener('mousedown', (e) => {
        if (e.target.tagName === 'BUTTON') return;
        dragging = true;
        const rect = uiElement.getBoundingClientRect();
        offset = [e.clientX - rect.left, e.clientY - rect.top];
    });
    document.addEventListener('mousemove', (e) => {
        if (!dragging) return;
        uiElement.style.left = `${e.clientX - offset[0]}px`;
        uiElement.style.top = `${e.clientY - offset[1]}px`;
    });
    document.addEventListener('mouseup', () => dragging = false);
 
    // Start
    requestAnimationFrame(pollFrame);
 
})();