MonkeyType (Educational Purpose Only)

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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");

})();