Human-Typer (Enhanced v1.7 - Advanced Algo) - Google Docs & Slides

Types text human-like with draggable UI, info popup, realistic typos, and advanced rhythm algorithm.

// ==UserScript==
// @name         Human-Typer (Enhanced v1.7 - Advanced Algo) - Google Docs & Slides
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Types text human-like with draggable UI, info popup, realistic typos, and advanced rhythm algorithm.
// @author       ∫(Ace)³dx (Enhanced by Claude)
// @match        https://docs.google.com/document/d/*
// @match        https://docs.google.com/presentation/d/*
// @icon         https://i.imgur.com/z2gxKWZ.png
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

/* globals GM_addStyle */

(function() {
    'use strict';

    // --- Configuration ---
    const DEFAULT_LOWER_BOUND = 60;
    const DEFAULT_UPPER_BOUND = 140;
    const DEFAULT_TYPO_RATE_PERCENT = 5;
    const DEFAULT_ENABLE_TYPOS = true;
    const DEFAULT_USE_ADVANCED_ALGORITHM = true; // Enable advanced by default

    // --- Basic Typo Config ---
    const MAX_TYPO_LENGTH = 3;
    const BASIC_TYPO_CHAR_DELAY_MS = 50;
    const BASIC_TYPO_PRE_BACKSPACE_DELAY_MS = 150;
    const BASIC_BACKSPACE_DELAY_MS = 90;

    // --- Advanced Algorithm Config ---
    const ADV_SPACE_MULTIPLIER_MIN = 1.8; // Multiplier for delay after a space
    const ADV_SPACE_MULTIPLIER_MAX = 2.8;
    const ADV_WORD_END_MULTIPLIER_MIN = 1.1; // Slight pause at end of word (before space/punctuation)
    const ADV_WORD_END_MULTIPLIER_MAX = 1.5;
    const ADV_PUNCTUATION_MULTIPLIER = 1.3; // Extra delay after typing punctuation like . , ! ?
    const ADV_RANDOM_PAUSE_CHANCE = 0.02; // Chance (2%) of a brief extra pause mid-typing
    const ADV_RANDOM_PAUSE_MIN_MS = 150;
    const ADV_RANDOM_PAUSE_MAX_MS = 400;
    // Advanced Typo Correction Delays
    const ADV_TYPO_RECOGNITION_MIN_MS = 250; // Time between finishing wrong chars and starting backspace
    const ADV_TYPO_RECOGNITION_MAX_MS = 800;
    const ADV_BACKSPACE_DELAY_MS = 100; // Slightly slower backspacing in advanced mode

    // --- State Variables ---
    let cancelTyping = false;
    let typingInProgress = false;
    let lowerBoundValue = DEFAULT_LOWER_BOUND;
    let upperBoundValue = DEFAULT_UPPER_BOUND;
    let enableTypos = DEFAULT_ENABLE_TYPOS;
    let typoRatePercentValue = DEFAULT_TYPO_RATE_PERCENT;
    let useAdvancedAlgorithm = DEFAULT_USE_ADVANCED_ALGORITHM; // State for advanced algo
    let overlayElement = null;
    let infoPopupElement = null;

    // --- CSS Styles ---
    GM_addStyle(`
        /* ... (Previous styles remain largely the same) ... */
        .human-typer-overlay {
            position: fixed; background-color: rgba(255, 255, 255, 0.95); padding: 0;
            border-radius: 8px; box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.2); z-index: 10000;
            display: flex; flex-direction: column; width: 360px; /* Slightly wider */
            border: 1px solid #ccc; font-family: sans-serif; font-size: 14px; color: #333;
        }
        .human-typer-header {
            background-color: #f1f1f1; padding: 8px 12px; cursor: move; border-bottom: 1px solid #ccc;
            border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex;
            justify-content: space-between; align-items: center; user-select: none;
        }
        .human-typer-header-title { font-weight: bold; }
        .human-typer-info-icon {
            cursor: pointer; font-style: normal; font-weight: bold; color: #d93025; border: 1px solid #d93025;
            border-radius: 50%; width: 18px; height: 18px; display: inline-flex; justify-content: center;
            align-items: center; font-size: 12px; margin-left: 10px; background-color: white;
        }
        .human-typer-info-icon:hover { background-color: #fce8e6; }
        .human-typer-content { padding: 15px; display: flex; flex-direction: column; gap: 12px; }
        .human-typer-overlay textarea {
            width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; resize: vertical;
            box-sizing: border-box; min-height: 80px; font-family: inherit;
        }
        .human-typer-label { font-size: 13px; color: #555; }
        .human-typer-input-group { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
        .human-typer-input-group label {
            flex-basis: 115px; /* Adjusted label width */ text-align: right; flex-shrink: 0;
        }
        .human-typer-input-group input[type="number"] {
            width: 60px; padding: 6px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
        }
        .human-typer-options-group { /* Group checkboxes */
             display: flex;
             flex-direction: column; /* Stack checkboxes */
             gap: 8px;
             margin-top: 5px;
             padding-left: 10px; /* Indent options */
        }
        .human-typer-checkbox-item { /* Style each checkbox line */
             display: flex;
             align-items: center;
             gap: 8px;
        }
         .human-typer-checkbox-item label {
             /* Labels next to checkbox don't need fixed width */
             flex-basis: auto;
             text-align: left;
         }
         .human-typer-checkbox-item input[type="number"] {
             width: 55px; padding: 6px; box-sizing: border-box;
         }
         .human-typer-checkbox-item .rate-label { /* Specific label for rate */
             margin-left: 10px;
             white-space: nowrap; /* Prevent wrapping */
         }
        .human-typer-eta { font-size: 12px; color: #777; min-height: 1.2em; text-align: center; }
        .human-typer-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
        .human-typer-button {
            padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
            transition: background-color 0.3s, color 0.3s; font-size: 14px;
        }
        .human-typer-confirm-button { background-color: #1a73e8; color: white; }
        .human-typer-confirm-button:hover:not(:disabled) { background-color: #1765cc; }
        .human-typer-confirm-button:disabled { opacity: 0.6; cursor: not-allowed; }
        .human-typer-cancel-button { background-color: #e0e0e0; color: #333; }
        .human-typer-cancel-button:hover { background-color: #d5d5d5; }
        .human-typer-info-popup {
            position: absolute; background-color: #fff; border: 1px solid #ccc; border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2); padding: 12px; font-size: 13px;
            max-width: 340px; /* Slightly wider for more text */ z-index: 10001; color: #333;
        }
        .human-typer-info-popup p { margin-top: 0; margin-bottom: 0.7em; line-height: 1.4; }
        .human-typer-info-popup p:last-child { margin-bottom: 0; }
        .human-typer-info-popup strong { color: #111; }
        .human-typer-info-popup code { background-color: #f0f0f0; padding: 1px 3px; border-radius: 3px; font-size: 12px;}
    `);

    // --- Main Logic ---
    function initializeScript() { console.log("Human-Typer initializing..."); insertButtons(); }
    function insertButtons() { /* ... (same as v1.6) ... */
        const helpMenu = document.getElementById("docs-help-menu");
        if (!helpMenu || document.getElementById("human-typer-button")) return;
        const humanTyperButton = createButton("Human-Typer", "human-typer-button");
        humanTyperButton.addEventListener("click", handleHumanTyperClick);
        const stopButton = createButton("Stop", "stop-button", true);
        stopButton.style.color = "red";
        stopButton.addEventListener("click", handleStopClick);
        helpMenu.parentNode.insertBefore(humanTyperButton, helpMenu);
        humanTyperButton.parentNode.insertBefore(stopButton, humanTyperButton.nextSibling);
        console.log("Human-Typer buttons inserted.");
    }
    function createButton(text, id, hidden = false) { /* ... (same as v1.6) ... */
        const button = document.createElement("div");
        button.textContent = text;
        button.classList.add("menu-button", "goog-control", "goog-inline-block");
        button.style.userSelect = "none"; button.style.cursor = "pointer"; button.style.transition = "background-color 0.2s, box-shadow 0.2s";
        button.id = id; if (hidden) button.style.display = "none";
        button.addEventListener("mouseenter", () => button.classList.add("goog-control-hover"));
        button.addEventListener("mouseleave", () => button.classList.remove("goog-control-hover"));
        return button;
    }
    function handleHumanTyperClick() { /* ... (same as v1.6, includes flash effect) ... */
        if (typingInProgress) {
            console.log("Typing already in progress.");
            const stopButton = document.getElementById("stop-button");
            if (stopButton) {
                 stopButton.style.opacity = '0.5'; setTimeout(() => { stopButton.style.opacity = '1'; }, 150);
                 setTimeout(() => { stopButton.style.opacity = '0.5'; }, 300); setTimeout(() => { stopButton.style.opacity = '1'; }, 450);
            } return;
        }
        if (!overlayElement) showOverlay(); else overlayElement.style.display = 'flex';
    }
    function handleStopClick() { /* ... (same as v1.6) ... */
        if (typingInProgress) {
            console.log("Stop requested."); cancelTyping = true;
            const stopButton = document.getElementById("stop-button");
            if (stopButton) { stopButton.textContent = "Stopping..."; stopButton.style.cursor = "default"; stopButton.classList.remove("goog-control-hover"); }
        }
    }

    function showOverlay() {
        if (overlayElement) { overlayElement.style.display = 'flex'; return; }

        overlayElement = document.createElement("div");
        overlayElement.classList.add("human-typer-overlay");

        // --- Header (same) ---
        const header = document.createElement("div"); header.classList.add("human-typer-header");
        const title = document.createElement("span"); title.classList.add("human-typer-header-title"); title.textContent = "Human-Typer Settings";
        const infoIcon = document.createElement("i"); infoIcon.classList.add("human-typer-info-icon"); infoIcon.textContent = "i"; infoIcon.title = "Show Instructions";
        infoIcon.addEventListener("click", toggleInfoPopup);
        header.appendChild(title); header.appendChild(infoIcon); overlayElement.appendChild(header);

        // --- Content Area ---
        const content = document.createElement("div"); content.classList.add("human-typer-content");
        const textField = document.createElement("textarea"); textField.placeholder = "Paste or type your text here...";
        const etaLabel = document.createElement("div"); etaLabel.classList.add("human-typer-eta");

        // --- Delay Settings ---
const delayGroup = document.createElement("div");
delayGroup.classList.add("human-typer-input-group");

const lowerBoundContainer = document.createElement("div");
const lowerBoundLabel = document.createElement("label");
lowerBoundLabel.textContent = "Min Delay (ms):";
const lowerBoundInput = document.createElement("input");
lowerBoundInput.type = "number";
lowerBoundInput.min = "0";
lowerBoundInput.value = lowerBoundValue;
lowerBoundContainer.appendChild(lowerBoundLabel);
lowerBoundContainer.appendChild(lowerBoundInput);

const upperBoundContainer = document.createElement("div");
const upperBoundLabel = document.createElement("label");
upperBoundLabel.textContent = "Max Delay (ms):";
const upperBoundInput = document.createElement("input");
upperBoundInput.type = "number";
upperBoundInput.min = "0";
upperBoundInput.value = upperBoundValue;
upperBoundContainer.appendChild(upperBoundLabel);
upperBoundContainer.appendChild(upperBoundInput);

delayGroup.appendChild(lowerBoundContainer);
delayGroup.appendChild(upperBoundContainer);


        // --- Options Group (Typos + Advanced Algo) ---
        const optionsGroup = document.createElement("div"); optionsGroup.classList.add("human-typer-options-group");

        // --- Typo Settings ---
        const typoItem = document.createElement("div"); typoItem.classList.add("human-typer-checkbox-item");
        const typoCheckbox = document.createElement("input"); typoCheckbox.type = "checkbox"; typoCheckbox.id = "human-typer-typo-enable"; typoCheckbox.checked = enableTypos;
        const typoLabel = document.createElement("label"); typoLabel.textContent = "Enable Typos & Auto Correction"; typoLabel.htmlFor = "human-typer-typo-enable";
        const typoRateLabel = document.createElement("label"); typoRateLabel.textContent = "Typo Rate (%):"; typoRateLabel.htmlFor = "human-typer-typo-rate"; typoRateLabel.classList.add("rate-label");
        const typoRateInput = document.createElement("input"); typoRateInput.type = "number"; typoRateInput.id = "human-typer-typo-rate"; typoRateInput.min = "0"; typoRateInput.max = "100"; typoRateInput.step = "1"; typoRateInput.value = typoRatePercentValue; typoRateInput.disabled = !enableTypos;

        typoCheckbox.addEventListener('change', () => { typoRateInput.disabled = !typoCheckbox.checked; enableTypos = typoCheckbox.checked; updateEta(); });
        typoItem.appendChild(typoCheckbox); typoItem.appendChild(typoLabel); typoItem.appendChild(typoRateLabel); typoItem.appendChild(typoRateInput);
        optionsGroup.appendChild(typoItem);

        // --- Advanced Algorithm Setting ---
        const advancedItem = document.createElement("div"); advancedItem.classList.add("human-typer-checkbox-item");
        const advancedCheckbox = document.createElement("input"); advancedCheckbox.type = "checkbox"; advancedCheckbox.id = "human-typer-advanced-algo"; advancedCheckbox.checked = useAdvancedAlgorithm;
        const advancedLabel = document.createElement("label"); advancedLabel.textContent = "Use Advanced Algorithm (Rhythm/Pauses)"; advancedLabel.htmlFor = "human-typer-advanced-algo";

        advancedCheckbox.addEventListener('change', () => { useAdvancedAlgorithm = advancedCheckbox.checked; updateEta(); });
        advancedItem.appendChild(advancedCheckbox); advancedItem.appendChild(advancedLabel);
        optionsGroup.appendChild(advancedItem);


        // --- Buttons ---
        const buttonContainer = document.createElement("div"); buttonContainer.classList.add("human-typer-buttons");
        const confirmButton = document.createElement("button"); confirmButton.textContent = "Start Typing"; confirmButton.classList.add("human-typer-button", "human-typer-confirm-button");
        const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.classList.add("human-typer-button", "human-typer-cancel-button");

        // --- Assemble Content ---
        content.appendChild(textField);
        content.appendChild(etaLabel);
        content.appendChild(delayGroup);
        content.appendChild(optionsGroup); // Add the group of options
        content.appendChild(buttonContainer);
        buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(confirmButton);
        overlayElement.appendChild(content);

        document.body.appendChild(overlayElement);

        // --- Center Overlay ---
        overlayElement.style.left = `${Math.max(0, (window.innerWidth - overlayElement.offsetWidth) / 2)}px`;
        overlayElement.style.top = `${Math.max(0, (window.innerHeight - overlayElement.offsetHeight) / 2)}px`;

        // --- Event Listeners & ETA ---
        const updateEta = () => {
            const charCount = textField.value.length;
            const low = parseInt(lowerBoundInput.value) || 0;
            const high = parseInt(upperBoundInput.value) || 0;
            if (charCount > 0 && low >= 0 && high >= low) {
                let baseMs = charCount * ((low + high) / 2); // Average base time
                let factor = 1.0;
                if (enableTypos) {
                     // Estimate typo overhead (typing wrong + backspacing)
                     // Average typo length ~MAX_TYPO_LENGTH/2. Delay per wrong char + backspace.
                     const avgTypoLen = MAX_TYPO_LENGTH / 2;
                     const typoTimePerOccur = avgTypoLen * (BASIC_TYPO_CHAR_DELAY_MS + (useAdvancedAlgorithm ? ADV_BACKSPACE_DELAY_MS : BASIC_BACKSPACE_DELAY_MS))
                                            + (useAdvancedAlgorithm ? (ADV_TYPO_RECOGNITION_MIN_MS + ADV_TYPO_RECOGNITION_MAX_MS)/2 : BASIC_TYPO_PRE_BACKSPACE_DELAY_MS);
                     baseMs += charCount * (typoRatePercentValue / 100) * typoTimePerOccur;
                }
                 if (useAdvancedAlgorithm) {
                      // Estimate overhead from pauses (spaces, ends, random) - very approximate
                      const spaceCount = (textField.value.match(/ /g) || []).length;
                      const avgSpacePauseIncrease = ((ADV_SPACE_MULTIPLIER_MIN + ADV_SPACE_MULTIPLIER_MAX) / 2 - 1) * ((low + high) / 2);
                      baseMs += spaceCount * avgSpacePauseIncrease;
                      // Add small fudge factor for other pauses
                      factor += 0.1;
                 }

                 const etaMinutes = Math.ceil(baseMs / 60000);
                 etaLabel.textContent = `ETA: ~${etaMinutes} minutes ${useAdvancedAlgorithm ? '(Advanced)' : ''} ${enableTypos ? '(incl. typos)' : ''}`;

            } else {
                etaLabel.textContent = "";
            }
            // Validate inputs
            const currentTypoRate = parseInt(typoRateInput.value);
            const typoRateValid = !enableTypos || (!isNaN(currentTypoRate) && currentTypoRate >= 0 && currentTypoRate <= 100); // Valid if typos disabled or rate is ok
            confirmButton.disabled = textField.value.trim() === "" || low < 0 || high < low || !typoRateValid;
        };

        textField.addEventListener("input", updateEta);
        lowerBoundInput.addEventListener("input", updateEta);
        upperBoundInput.addEventListener("input", updateEta);
        typoCheckbox.addEventListener("change", updateEta); // Handled above
        advancedCheckbox.addEventListener("change", updateEta); // Handled above
        typoRateInput.addEventListener("input", () => {
            const rate = parseInt(typoRateInput.value); if (!isNaN(rate)) typoRatePercentValue = rate;
            updateEta(); // Recalculate
        });

        cancelButton.addEventListener("click", () => { overlayElement.style.display = 'none'; hideInfoPopup(); });

        confirmButton.addEventListener("click", () => {
            const userInput = textField.value;
            const newLower = parseInt(lowerBoundInput.value); const newUpper = parseInt(upperBoundInput.value);
            const newTypoRatePercent = parseInt(typoRateInput.value);
            const newEnableTypos = typoCheckbox.checked;
            const newUseAdvanced = advancedCheckbox.checked;

            if (userInput.trim() === "" || isNaN(newLower) || isNaN(newUpper) || newLower < 0 || newUpper < newLower || (newEnableTypos && (isNaN(newTypoRatePercent) || newTypoRatePercent < 0 || newTypoRatePercent > 100))) {
                 console.warn("Invalid input or settings."); return;
             }

            lowerBoundValue = newLower; upperBoundValue = newUpper;
            enableTypos = newEnableTypos; typoRatePercentValue = newTypoRatePercent;
            useAdvancedAlgorithm = newUseAdvanced; // Store advanced setting

            overlayElement.style.display = 'none'; hideInfoPopup();
            startTypingProcess(userInput);
        });

        makeDraggable(overlayElement, header); // Make draggable
        updateEta(); // Initial calculation
    }

    function toggleInfoPopup(event) { /* ... (same as v1.6) ... */ if (infoPopupElement) hideInfoPopup(); else showInfoPopup(event.target); event.stopPropagation(); }

    function showInfoPopup(iconElement) {
        hideInfoPopup();
        infoPopupElement = document.createElement('div');
        infoPopupElement.classList.add('human-typer-info-popup');
        infoPopupElement.innerHTML = `
            <p><strong>Instructions:</strong></p>
            <p>- Paste text into the area.</p>
            <p>- Set <strong>Min/Max Delay</strong> (ms) for base character typing speed.</p>
            <p>- Enable <strong>Typos</strong> & set <strong>% Rate</strong>. Typos involve typing adjacent keys, pausing, then auto-correcting with Backspace.</p>
            <p>- Enable <strong>Advanced Algorithm</strong> for more human-like rhythm:</p>
            <p style="margin-left: 15px; margin-bottom: 0.3em;">• Longer pauses after spaces.</p>
            <p style="margin-left: 15px; margin-bottom: 0.3em;">• Slight pauses before punctuation/end-of-word.</p>
            <p style="margin-left: 15px; margin-bottom: 0.3em;">• Longer, variable pause before correcting typos (<code>${ADV_TYPO_RECOGNITION_MIN_MS}-${ADV_TYPO_RECOGNITION_MAX_MS}ms</code>).</p>
            <p style="margin-left: 15px; margin-bottom: 0.7em;">• Occasional random brief pauses.</p>
            <p>- Click <strong>'Start Typing'</strong> (ensure cursor is in Doc/Slide).</p>
            <p>- Keep tab active. Use <strong>'Stop'</strong> button to cancel.</p>
            <p>- Drag window header to move.</p>
        `;
        document.body.appendChild(infoPopupElement);
        // Position the popup (same logic as v1.6)
        const iconRect = iconElement.getBoundingClientRect(); const popupRect = infoPopupElement.getBoundingClientRect();
        let top = iconRect.bottom + window.scrollY + 5; let left = iconRect.left + window.scrollX - (popupRect.width / 2) + (iconRect.width / 2);
        const margin = 10; if (left < margin) left = margin; if (left + popupRect.width > window.innerWidth - margin) left = window.innerWidth - popupRect.width - margin;
        if (top + popupRect.height > window.innerHeight - margin) top = iconRect.top + window.scrollY - popupRect.height - 5; if (top < margin) top = margin;
        infoPopupElement.style.top = `${top}px`; infoPopupElement.style.left = `${left}px`;
        setTimeout(() => { document.addEventListener('click', handleClickOutsideInfoPopup, true); }, 0);
    }

    function hideInfoPopup() { /* ... (same as v1.6) ... */ if (infoPopupElement) { infoPopupElement.remove(); infoPopupElement = null; document.removeEventListener('click', handleClickOutsideInfoPopup, true); } }
    function handleClickOutsideInfoPopup(event) { /* ... (same as v1.6) ... */ if (infoPopupElement && !infoPopupElement.contains(event.target) && !event.target.classList.contains('human-typer-info-icon')) hideInfoPopup(); }
    function makeDraggable(element, handle) { /* ... (same as v1.6, includes boundary checks) ... */
        let isDragging = false; let offsetX, offsetY;
        const onMouseDown = (e) => { if (e.button !== 0) return; if (!handle || handle.contains(e.target)) { isDragging = true; const rect = element.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; element.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; e.preventDefault(); } };
        const onMouseMove = (e) => { if (!isDragging) return; let newX = e.clientX - offsetX; let newY = e.clientY - offsetY; const vw = window.innerWidth; const vh = window.innerHeight; const elemWidth = element.offsetWidth; const elemHeight = element.offsetHeight; const margin = 5; newX = Math.max(margin, Math.min(newX, vw - elemWidth - margin)); newY = Math.max(margin, Math.min(newY, vh - elemHeight - margin)); element.style.left = `${newX}px`; element.style.top = `${newY}px`; };
        const onMouseUp = (e) => { if (isDragging && e.button === 0) { isDragging = false; element.style.cursor = ''; document.body.style.userSelect = ''; if (handle) handle.style.cursor = 'move'; } };
        const target = handle || element; target.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); if (handle) handle.style.cursor = 'move';
    }

    // --- Typing Simulation ---

    async function startTypingProcess(textToType) { /* ... (same setup as v1.6) ... */
        const inputElement = findInputElement(); if (!inputElement) { alert("Could not find the Google Docs/Slides input area. Ensure cursor is active."); return; }
        typingInProgress = true; cancelTyping = false; const stopButton = document.getElementById("stop-button");
        if (stopButton) { stopButton.textContent = "Stop"; stopButton.style.display = "inline-block"; stopButton.style.cursor = "pointer"; }
        await typeStringWithLogic(inputElement, textToType);
        typingInProgress = false; if (stopButton) stopButton.style.display = "none";
        console.log(cancelTyping ? "Typing stopped by user." : "Typing finished.");
    }

    function findInputElement() { /* ... (same as v1.6) ... */
         try { const iframe = document.querySelector(".docs-texteventtarget-iframe"); if (iframe && iframe.contentDocument) { return iframe.contentDocument.activeElement && iframe.contentDocument.activeElement.nodeName !== 'HTML' ? iframe.contentDocument.activeElement : iframe.contentDocument.body; } } catch (e) { console.error("Error accessing iframe content:", e); } console.error("Could not find target input element."); return null;
    }

    // Helper function for delays, cancellable
    async function delay(ms) { /* ... (same as v1.6) ... */
        return new Promise(resolve => { if (ms <= 0) { resolve(!cancelTyping); return; } const timeoutId = setTimeout(() => resolve(!cancelTyping), ms); const checkCancel = () => { if (cancelTyping) { clearTimeout(timeoutId); resolve(false); } else if (typingInProgress) { requestAnimationFrame(checkCancel); } }; requestAnimationFrame(checkCancel); });
    }

    // Calculates delay based on mode and context
    function calculateDelay(currentChar, prevChar, nextChar) {
        let baseDelay = Math.random() * (upperBoundValue - lowerBoundValue) + lowerBoundValue;
        let finalDelay = baseDelay;

        if (useAdvancedAlgorithm) {
            const isSpace = (char) => char === ' ';
            const isPunctuation = (char) => /[.,!?;:]/.test(char); // Basic punctuation
            const isEndOfWord = (current, next) => !isSpace(current) && (isSpace(next) || next === null || next === '\n');

            // Pause after space
            if (prevChar && isSpace(prevChar)) {
                finalDelay *= Math.random() * (ADV_SPACE_MULTIPLIER_MAX - ADV_SPACE_MULTIPLIER_MIN) + ADV_SPACE_MULTIPLIER_MIN;
            }
            // Pause after punctuation
            else if (prevChar && isPunctuation(prevChar)) {
                 finalDelay *= ADV_PUNCTUATION_MULTIPLIER;
            }
             // Slight pause at end of word (before space/newline/end)
            else if (isEndOfWord(currentChar, nextChar)) {
                finalDelay *= Math.random() * (ADV_WORD_END_MULTIPLIER_MAX - ADV_WORD_END_MULTIPLIER_MIN) + ADV_WORD_END_MULTIPLIER_MIN;
            }

            // Occasional random longer pause
            if (Math.random() < ADV_RANDOM_PAUSE_CHANCE) {
                const pause = Math.random() * (ADV_RANDOM_PAUSE_MAX_MS - ADV_RANDOM_PAUSE_MIN_MS) + ADV_RANDOM_PAUSE_MIN_MS;
                console.log(`-- Random pause: ${pause.toFixed(0)}ms --`);
                finalDelay += pause;
            }
        }

        // Ensure delay isn't negative or excessively small
        return Math.max(10, finalDelay);
    }


    async function simulateKey(inputElement, charOrCode, keyDelay) {
        // Use the delay helper BEFORE dispatching
        const proceed = await delay(keyDelay);
        if (!proceed) return false; // Stop if cancelled during delay

        const eventProps = { bubbles: true, cancelable: true };
        let eventType; let keyEventProps; let logChar = charOrCode;

        if (charOrCode === '\n') {
            eventType = 'keydown'; keyEventProps = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 }; logChar = '\\n';
        } else if (charOrCode === '\b') { // Backspace
            eventType = 'keydown'; keyEventProps = { key: 'Backspace', code: 'Backspace', keyCode: 8, which: 8 }; logChar = 'Backspace';
        }
        else { // Regular character
            eventType = 'keypress'; keyEventProps = { key: charOrCode, charCode: charOrCode.charCodeAt(0), keyCode: charOrCode.charCodeAt(0), which: charOrCode.charCodeAt(0) };
        }

        Object.assign(eventProps, keyEventProps);
        const eventObj = new KeyboardEvent(eventType, eventProps);

        try {
            inputElement.dispatchEvent(eventObj);
            console.log(`Key: ${logChar}, Delay: ${keyDelay.toFixed(0)}ms`);
        } catch (e) {
            console.error(`Error dispatching event for key "${logChar}":`, e);
            return false; // Indicate failure
        }
        return true; // Indicate success
    }

    // Main typing loop using the logic/delay calculation
    async function typeStringWithLogic(inputElement, string) {
        for (let i = 0; i < string.length; i++) {
            if (cancelTyping) break;

            const char = string[i];
            const prevChar = i > 0 ? string[i - 1] : null;
            const nextChar = i < string.length - 1 ? string[i + 1] : null;

            let proceed = true;

            // --- Typo Simulation ---
            if (enableTypos && char.match(/\S/) && char !== '\n' && Math.random() < (typoRatePercentValue / 100)) {
                const typoLength = Math.floor(Math.random() * MAX_TYPO_LENGTH) + 1;
                let wrongSequence = "";
                for (let j = 0; j < typoLength; j++) {
                    wrongSequence += getNearbyKey(char); // Use original char for adjacency basis
                }

                console.log(`-> Simulating ${typoLength}-char typo for '${char}', typing '${wrongSequence}'`);

                // 1. Type the wrong sequence
                for (let j = 0; j < wrongSequence.length; j++) {
                    proceed = await simulateKey(inputElement, wrongSequence[j], BASIC_TYPO_CHAR_DELAY_MS); // Quick typing for wrong chars
                    if (!proceed) break;
                }
                if (!proceed) break;

                // 2. Pause before correcting (variable if advanced)
                const recognitionDelay = useAdvancedAlgorithm
                    ? Math.random() * (ADV_TYPO_RECOGNITION_MAX_MS - ADV_TYPO_RECOGNITION_MIN_MS) + ADV_TYPO_RECOGNITION_MIN_MS
                    : BASIC_TYPO_PRE_BACKSPACE_DELAY_MS;
                console.log(`-- Typo recognition pause: ${recognitionDelay.toFixed(0)}ms --`);
                proceed = await delay(recognitionDelay);
                if (!proceed) break;

                // 3. Delete the wrong sequence
                const backspaceDelay = useAdvancedAlgorithm ? ADV_BACKSPACE_DELAY_MS : BASIC_BACKSPACE_DELAY_MS;
                for (let j = 0; j < wrongSequence.length; j++) {
                    proceed = await simulateKey(inputElement, '\b', backspaceDelay); // Use backspace delay
                    if (!proceed) break;
                }
                if (!proceed) break;

                console.log(`<- Typo for '${char}' corrected.`);
            }
            // --- End Typo Simulation ---

            // Calculate delay for the *correct* character using context
            const typingDelay = calculateDelay(char, prevChar, nextChar);

            // Type the correct character
            proceed = await simulateKey(inputElement, char, typingDelay);
            if (!proceed) break;
        }
    }

    // --- Typo Helper ---
    function getNearbyKey(char) { /* ... (same as v1.6, includes number/symbol attempt) ... */
        const keyboardLayout={'q':'wa','w':'qase','e':'wsdr','r':'edft','t':'rfgy','y':'tghu','u':'yhji','i':'ujko','o':'iklp','p':'ol[','a':'qwsz','s':'awedxz','d':'erfcxs','f':'rtgvcd','g':'tyhbvf','h':'yujnbg','j':'uikmnh','k':'iolmj','l':'opk;','z':'asx','x':'zsdc','c':'xdfv','v':'cfgb','b':'vghn','n':'bhjm','m':'njk,','1':'2q`','2':'1qw3','3':'2we4','4':'3er5','5':'4rt6','6':'5ty7','7':'6yu8','8':'7ui9','9':'8io0','0':'9op-','-':'0p[=','=':'-[]','[':'=p]o',']':'[\\;p','\\':']=',';':'lkp\'[]',"'":';l/',',':'mkj.','m':'.', '/':'\'l;.,', '`':'1', ' ':' '}; // Added space mapping to itself
        const lowerChar = char.toLowerCase(); const adjacent = keyboardLayout[lowerChar];
        if (!adjacent || adjacent.length === 0) return char;
        let attempts = 0; let nearbyChar;
        do { const randomIndex = Math.floor(Math.random() * adjacent.length); nearbyChar = adjacent[randomIndex]; attempts++; } while (nearbyChar === lowerChar && attempts < 5 && adjacent.length > 1)
        return char === char.toUpperCase() && char !== lowerChar ? nearbyChar.toUpperCase() : nearbyChar;
    }

    // --- Initialization ---
    const initInterval = setInterval(() => { /* ... (same robust check as v1.6) ... */
        if (document.getElementById("docs-help-menu") && document.querySelector(".docs-texteventtarget-iframe")) {
            try { if (document.querySelector(".docs-texteventtarget-iframe").contentDocument) { clearInterval(initInterval); initializeScript(); } else { console.log("Human-Typer: Waiting for iframe content access..."); } } catch (e) { console.log("Human-Typer: Waiting for iframe content access (error)..."); }
        } }, 500);

})(); // End userscript

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址