MonkeyType (Educational Purpose Only)

Smart typing assistant with natural behavior. Press "/" to activate.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name MonkeyType (Educational Purpose Only)
// @author Kamron
// @description Smart typing assistant with natural behavior. Press "/" to activate.
// @icon https://monkeytype.com/images/favicon/favicon-32x32.png
// @version 1.0
// @match *://monkeytype.com/*
// @run-at document-idle
// @grant none
// @license MIT
// @namespace https://greasyfork.org/users/1542493
// ==/UserScript==

(function() {
    "use strict";

    const TRIGGER_KEY = "Slash";

    let config = {
        speed: 85,
        precision: 94,
        autoFix: true
    };

    let active = false;
    let sessionData = {
        charCount: 0,
        mistakeCount: 0,
        beginTime: null,
        lastMistakeTime: 0,
        streakCount: 0,
        inMistakeChain: false
    };

    const NEARBY_KEYS = {
        'q': ['w', 'a', 's'],
        'w': ['q', 'e', 'a', 's', 'd'],
        'e': ['w', 'r', 's', 'd', 'f'],
        'r': ['e', 't', 'd', 'f', 'g'],
        't': ['r', 'y', 'f', 'g', 'h'],
        'y': ['t', 'u', 'g', 'h', 'j'],
        'u': ['y', 'i', 'h', 'j', 'k'],
        'i': ['u', 'o', 'j', 'k', 'l'],
        'o': ['i', 'p', 'k', 'l'],
        'p': ['o', 'l'],
        'a': ['q', 'w', 's', 'z', 'x'],
        's': ['q', 'w', 'e', 'a', 'd', 'z', 'x', 'c'],
        'd': ['w', 'e', 'r', 's', 'f', 'x', 'c', 'v'],
        'f': ['e', 'r', 't', 'd', 'g', 'c', 'v', 'b'],
        'g': ['r', 't', 'y', 'f', 'h', 'v', 'b', 'n'],
        'h': ['t', 'y', 'u', 'g', 'j', 'b', 'n', 'm'],
        'j': ['y', 'u', 'i', 'h', 'k', 'n', 'm'],
        'k': ['u', 'i', 'o', 'j', 'l', 'm'],
        'l': ['i', 'o', 'p', 'k'],
        'z': ['a', 's', 'x'],
        'x': ['a', 's', 'd', 'z', 'c'],
        'c': ['s', 'd', 'f', 'x', 'v'],
        'v': ['d', 'f', 'g', 'c', 'b'],
        'b': ['f', 'g', 'h', 'v', 'n'],
        'n': ['g', 'h', 'j', 'b', 'm'],
        'm': ['h', 'j', 'k', 'n'],
        ' ': [' ']
    };

    const TRICKY_PAIRS = ['qu', 'xe', 'xc', 'zx', 'bv', 'uy', 'ij', 'kj', 'pq'];

    let prevChar = '';

    function randRange(low, high) {
        return Math.random() * (high - low) + low;
    }

    function calcInterval() {
        const base = 60000 / (config.speed * 5);
        const n1 = Math.random();
        const n2 = Math.random();
        const curve = Math.sqrt(-2 * Math.log(n1)) * Math.cos(2 * Math.PI * n2);
        const factor = Math.max(0.5, Math.min(2, 1 + curve * 0.25));
        return base * factor;
    }

    function calcWordGap() {
        const base = 60000 / (config.speed * 5);
        return base * randRange(1.2, 2.5);
    }

    function isReady() {
        const testArea = document.getElementById("typingTest");
        if (!testArea) return false;
        const hidden = testArea.classList.contains("hidden");
        if (hidden) active = false;
        return active && !hidden;
    }

    function fetchNextChar() {
        const currentWord = document.querySelector(".word.active");
        if (!currentWord) return null;
        for (const letter of currentWord.children) {
            if (letter.className === "") return letter.textContent;
        }
        return " ";
    }

    function getNearbyKey(char) {
        const lower = char.toLowerCase();
        const nearby = NEARBY_KEYS[lower];
        if (!nearby || nearby.length === 0) {
            const fallback = 'asdfghjkl';
            return fallback[Math.floor(Math.random() * fallback.length)];
        }
        const options = nearby.filter(k => k !== lower);
        if (options.length === 0) return nearby[0];
        return options[Math.floor(Math.random() * options.length)];
    }

    function willMistake(char) {
        const baseRate = (100 - config.precision) / 100;
        let chance = baseRate;

        const timeSinceLast = Date.now() - sessionData.lastMistakeTime;
        if (timeSinceLast < 2000 && sessionData.lastMistakeTime > 0) {
            if (sessionData.inMistakeChain) {
                chance *= 2.5;
            }
        } else {
            sessionData.inMistakeChain = false;
        }

        if (sessionData.streakCount > 30) {
            chance *= 0.5;
        } else if (sessionData.streakCount > 15) {
            chance *= 0.7;
        }

        if (sessionData.beginTime) {
            const elapsed = (Date.now() - sessionData.beginTime) / 1000;
            if (elapsed > 45) {
                chance *= 1.3;
            } else if (elapsed > 25) {
                chance *= 1.15;
            }
        }

        const pair = (prevChar + char).toLowerCase();
        if (TRICKY_PAIRS.includes(pair)) {
            chance *= 1.8;
        }

        if (Math.random() < 0.1) {
            chance *= randRange(0.2, 2.5);
        }

        const result = Math.random() < chance;

        if (result) {
            if (Math.random() < 0.3) {
                sessionData.inMistakeChain = true;
            }
            sessionData.lastMistakeTime = Date.now();
            sessionData.streakCount = 0;
            sessionData.mistakeCount++;
        } else {
            sessionData.streakCount++;
        }

        sessionData.charCount++;
        return result;
    }

    function pickMistakeType() {
        const roll = Math.random();
        if (roll < 0.65) return 'nearby';
        if (roll < 0.85) return 'skip';
        return 'double';
    }

    function typeChar(char) {
        const field = document.getElementById("wordsInput");
        if (!field) return false;
        field.focus();
        return document.execCommand("insertText", false, char);
    }

    function eraseChar() {
        const field = document.getElementById("wordsInput");
        if (!field) return false;
        field.focus();
        if (field.value.length > 0) {
            field.value = field.value.slice(0, -1);
            field.dispatchEvent(new InputEvent('input', {
                inputType: 'deleteContentBackward',
                bubbles: true
            }));
            return true;
        }
        return false;
    }

    function processChar() {
        if (!isReady()) {
            console.log("[Keyflow] Stopped");
            refreshStatus(false);
            return;
        }

        const nextChar = fetchNextChar();

        if (nextChar === null) {
            setTimeout(processChar, 100);
            return;
        }

        if (!sessionData.beginTime) {
            sessionData.beginTime = Date.now();
        }

        if (willMistake(nextChar) && nextChar !== " ") {
            const mistakeType = pickMistakeType();

            switch (mistakeType) {
                case 'nearby':
                    doNearbyMistake(nextChar);
                    break;
                case 'skip':
                    doSkipMistake(nextChar);
                    break;
                case 'double':
                    doDoubleMistake(nextChar);
                    break;
                default:
                    doNearbyMistake(nextChar);
            }
        } else {
            typeChar(nextChar);
            prevChar = nextChar;
            const wait = nextChar === " " ? calcWordGap() : calcInterval();
            setTimeout(processChar, wait);
        }
    }

    function doNearbyMistake(correct) {
        const wrong = getNearbyKey(correct);
        typeChar(wrong);

        if (config.autoFix) {
            const notice = randRange(150, 400);
            setTimeout(() => {
                eraseChar();
                setTimeout(() => {
                    typeChar(correct);
                    prevChar = correct;
                    setTimeout(processChar, calcInterval());
                }, randRange(80, 200));
            }, notice);
        } else {
            prevChar = wrong;
            setTimeout(processChar, calcInterval());
        }
    }

    function doSkipMistake(correct) {
        doNearbyMistake(correct);
    }

    function doDoubleMistake(correct) {
        typeChar(correct);

        setTimeout(() => {
            typeChar(correct);

            if (config.autoFix) {
                setTimeout(() => {
                    eraseChar();
                    prevChar = correct;
                    setTimeout(processChar, calcInterval());
                }, randRange(100, 250));
            } else {
                prevChar = correct;
                setTimeout(processChar, calcInterval());
            }
        }, randRange(30, 80));
    }

    window.addEventListener("keydown", function(e) {
        if (e.code === TRIGGER_KEY) {
            e.preventDefault();
            if (e.repeat) return;

            active = !active;
            refreshStatus(active);

            if (active) {
                sessionData = {
                    charCount: 0,
                    mistakeCount: 0,
                    beginTime: null,
                    lastMistakeTime: 0,
                    streakCount: 0,
                    inMistakeChain: false
                };
                prevChar = '';

                console.log("[Keyflow] Running");
                const field = document.getElementById("wordsInput");
                if (field) field.focus();
                setTimeout(processChar, 100);
            } else {
                console.log("[Keyflow] Paused");
            }
        }
    });

    function loadConfig() {
        try {
            const data = localStorage.getItem("keyflow_config_v1");
            if (data) {
                const parsed = JSON.parse(data);
                config = { ...config, ...parsed };
            }
        } catch (err) {}
    }

    function saveConfig() {
        localStorage.setItem("keyflow_config_v1", JSON.stringify(config));
    }

    let stateEl = null;

    function refreshStatus(isOn) {
        if (stateEl) {
            stateEl.textContent = isOn ? "ON" : "OFF";
            stateEl.className = `kf-state ${isOn ? 'active' : 'idle'}`;
        }
    }

    function initPanel() {
        if (document.getElementById("kf-panel")) return;
        loadConfig();

        const panel = document.createElement("div");
        panel.id = "kf-panel";
        panel.innerHTML = `
            <style>
                #kf-panel {
                    position: fixed;
                    bottom: 24px;
                    right: 24px;
                    background: #1e1e1e;
                    border: 1px solid #3a3a3a;
                    border-radius: 12px;
                    padding: 16px;
                    font-family: 'Roboto Mono', 'SF Mono', monospace;
                    font-size: 13px;
                    color: #d4d4d4;
                    z-index: 999999;
                    width: 220px;
                    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
                    user-select: none;
                }

                #kf-panel * {
                    box-sizing: border-box;
                }

                #kf-panel .kf-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 16px;
                    padding-bottom: 12px;
                    border-bottom: 1px solid #3a3a3a;
                }

                #kf-panel .kf-title {
                    font-weight: 600;
                    font-size: 14px;
                    color: #e2b714;
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }

                #kf-panel .kf-ver {
                    font-size: 10px;
                    color: #666;
                    font-weight: normal;
                }

                #kf-panel .kf-state {
                    font-size: 12px;
                    font-weight: 600;
                    padding: 4px 10px;
                    border-radius: 6px;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }

                #kf-panel .kf-state.active {
                    background: rgba(76, 175, 80, 0.2);
                    color: #4CAF50;
                }

                #kf-panel .kf-state.idle {
                    background: rgba(244, 67, 54, 0.2);
                    color: #f44336;
                }

                #kf-panel .kf-ctrl {
                    margin-bottom: 14px;
                }

                #kf-panel .kf-label {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 8px;
                    font-size: 12px;
                    color: #888;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }

                #kf-panel .kf-val {
                    font-weight: 600;
                    color: #e2b714;
                    font-size: 13px;
                }

                #kf-panel input[type="range"] {
                    width: 100%;
                    height: 6px;
                    border-radius: 3px;
                    background: #3a3a3a;
                    outline: none;
                    -webkit-appearance: none;
                    cursor: pointer;
                }

                #kf-panel input[type="range"]::-webkit-slider-thumb {
                    -webkit-appearance: none;
                    width: 16px;
                    height: 16px;
                    border-radius: 50%;
                    background: #e2b714;
                    cursor: pointer;
                    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
                    transition: transform 0.15s ease;
                }

                #kf-panel input[type="range"]::-webkit-slider-thumb:hover {
                    transform: scale(1.1);
                }

                #kf-panel input[type="range"]::-moz-range-thumb {
                    width: 16px;
                    height: 16px;
                    border-radius: 50%;
                    background: #e2b714;
                    cursor: pointer;
                    border: none;
                }

                #kf-panel .kf-toggle {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                    padding: 10px 0;
                    border-top: 1px solid #3a3a3a;
                    margin-top: 14px;
                }

                #kf-panel .kf-switch {
                    position: relative;
                    width: 40px;
                    height: 22px;
                    flex-shrink: 0;
                }

                #kf-panel .kf-switch input {
                    opacity: 0;
                    width: 0;
                    height: 0;
                }

                #kf-panel .kf-track {
                    position: absolute;
                    cursor: pointer;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: #3a3a3a;
                    border-radius: 22px;
                    transition: 0.3s;
                }

                #kf-panel .kf-track:before {
                    position: absolute;
                    content: "";
                    height: 16px;
                    width: 16px;
                    left: 3px;
                    bottom: 3px;
                    background: #888;
                    border-radius: 50%;
                    transition: 0.3s;
                }

                #kf-panel input:checked + .kf-track {
                    background: rgba(226, 183, 20, 0.3);
                }

                #kf-panel input:checked + .kf-track:before {
                    transform: translateX(18px);
                    background: #e2b714;
                }

                #kf-panel .kf-switchlabel {
                    font-size: 12px;
                    color: #888;
                }

                #kf-panel .kf-hint {
                    text-align: center;
                    margin-top: 14px;
                    padding-top: 12px;
                    border-top: 1px solid #3a3a3a;
                    font-size: 11px;
                    color: #666;
                }

                #kf-panel .kf-hint kbd {
                    background: #3a3a3a;
                    padding: 3px 8px;
                    border-radius: 4px;
                    font-family: inherit;
                    color: #d4d4d4;
                    margin: 0 2px;
                }
            </style>

            <div class="kf-header">
                <div class="kf-title">
                    <span>MonkeyType <span class="kf-ver">v1</span></span>
                </div>
                <span id="kf-state" class="kf-state idle">OFF</span>
            </div>

            <div class="kf-ctrl">
                <div class="kf-label">
                    <span>Speed</span>
                    <span class="kf-val"><span id="kf-speed">${config.speed}</span> WPM</span>
                </div>
                <input type="range" id="kf-speed-slider" min="30" max="180" value="${config.speed}">
            </div>

            <div class="kf-ctrl">
                <div class="kf-label">
                    <span>Accuracy</span>
                    <span class="kf-val"><span id="kf-precision">${config.precision}</span>%</span>
                </div>
                <input type="range" id="kf-precision-slider" min="85" max="100" value="${config.precision}">
            </div>

            <div class="kf-toggle">
                <label class="kf-switch">
                    <input type="checkbox" id="kf-autofix" ${config.autoFix ? 'checked' : ''}>
                    <span class="kf-track"></span>
                </label>
                <span class="kf-switchlabel">Auto-correct errors</span>
            </div>

            <div class="kf-hint">
                Press <kbd>/</kbd> to toggle
            </div>
        `;

        document.body.appendChild(panel);

        stateEl = document.getElementById("kf-state");
        const speedSlider = document.getElementById("kf-speed-slider");
        const speedVal = document.getElementById("kf-speed");
        const precisionSlider = document.getElementById("kf-precision-slider");
        const precisionVal = document.getElementById("kf-precision");
        const autofixToggle = document.getElementById("kf-autofix");

        speedSlider.addEventListener("input", () => {
            config.speed = parseInt(speedSlider.value);
            speedVal.textContent = config.speed;
            saveConfig();
        });

        precisionSlider.addEventListener("input", () => {
            config.precision = parseInt(precisionSlider.value);
            precisionVal.textContent = config.precision;
            saveConfig();
        });

        autofixToggle.addEventListener("change", () => {
            config.autoFix = autofixToggle.checked;
            saveConfig();
        });

        console.log("[Keyflow] Panel ready");
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", () => setTimeout(initPanel, 500));
    } else {
        setTimeout(initPanel, 500);
    }

    console.log("[Keyflow] Initialized");

})();