MonkeyType (Educational Purpose Only)

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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");

})();