@ertekinno/human-like
Version:
A sophisticated React typewriter effect library with realistic human typing behavior, mobile/desktop keyboards, and comprehensive theming support
1,954 lines • 72 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { useState, useRef, useEffect, useCallback } from "react";
const TIMING_CONSTANTS = {
BASE_SPEED: 80,
// Average milliseconds per character
SPEED_VARIATION: 40,
// Random timing variation (±40ms)
MIN_CHAR_DELAY: 25,
// Minimum delay between characters
// Punctuation and Structure
SENTENCE_PAUSE: 500,
// Pause after . ! ?
COMMA_PAUSE: 200,
// Pause after , ; :
WORD_SPACE: 150,
// Pause between words
LINE_BREAK: 800,
// Pause for new paragraphs
// Mistake Handling
REALIZATION_DELAY: 300,
// Time to notice mistake
CORRECTION_PAUSE: 250,
// Pause before retyping
BACKSPACE_SPEED: 60,
// Speed of corrections
// Human Behavior
THINKING_PAUSE: 400,
// Pause before complex words
FATIGUE_INCREMENT: 0.5,
// Gradual slowdown per 100 chars
BURST_SPEED_MULTIPLIER: 0.6,
// Speed multiplier during bursts
CONCENTRATION_PAUSE: 800,
// Duration of concentration lapses
// Enhanced Realism
SHIFT_HESITATION: 100,
// Extra delay for shift key characters (increased)
CAPS_LOCK_ON_DELAY: 150,
// Delay when turning CAPS LOCK on
CAPS_LOCK_OFF_DELAY: 100,
// Delay when turning CAPS LOCK off
CAPS_SEQUENCE_THRESHOLD: 3,
// Min consecutive caps to trigger CAPS LOCK mode
NUMBER_ROW_PENALTY: 35,
// Extra delay for number characters
SYMBOL_BASE_PENALTY: 25,
// Base extra delay for symbols
LOOK_AHEAD_CHANCE: 0.08
// 8% chance of look-ahead typing
};
const BEHAVIOR_RATES = {
MISTAKE_FREQUENCY: 0.03,
// 3% base mistake rate
CONCENTRATION_LAPSE: 0.03,
// 3% random pause chance
BURST_TYPING: 0.15,
// 15% rapid sequence chance
FATIGUE_FACTOR: 1e-4,
// Gradual slowdown rate
OVERCORRECTION_RATE: 0.2
// 20% chance of making mistake while correcting
};
const DESKTOP_ADJACENT = {
"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", ";", "'"],
"[": ["p", "]", ";", "'"],
"]": ["[", "\\", "'"],
"a": ["q", "w", "s", "z"],
"s": ["q", "w", "e", "a", "d", "z", "x"],
"d": ["w", "e", "r", "s", "f", "x", "c"],
"f": ["e", "r", "t", "d", "g", "c", "v"],
"g": ["r", "t", "y", "f", "h", "v", "b"],
"h": ["t", "y", "u", "g", "j", "b", "n"],
"j": ["y", "u", "i", "h", "k", "n", "m"],
"k": ["u", "i", "o", "j", "l", "m", ","],
"l": ["i", "o", "p", "k", ";", ",", "."],
";": ["o", "p", "[", "l", "'", ".", "/"],
"'": ["p", "[", "]", ";", "/", "."],
"z": ["a", "s", "x"],
"x": ["z", "s", "d", "c"],
"c": ["x", "d", "f", "v", " "],
"v": ["c", "f", "g", "b", " "],
"b": ["v", "g", "h", "n", " "],
"n": ["b", "h", "j", "m", " "],
"m": ["n", "j", "k", ",", " "],
",": ["m", "k", "l", "."],
".": [",", "l", ";", "/", "'"],
"/": [".", ";", "'"],
" ": ["c", "v", "b", "n", "m"]
// Space bar adjacent to bottom row
};
const MOBILE_ADJACENT = {
// Top row - mobile keyboards often have tighter spacing
"q": ["w", "a", "s"],
// Less diagonal mistakes on mobile
"w": ["q", "e", "s", "a"],
// More focused on immediate neighbors
"e": ["w", "r", "d", "s"],
"r": ["e", "t", "f", "d"],
"t": ["r", "y", "g", "f"],
"y": ["t", "u", "h", "g"],
"u": ["y", "i", "j", "h"],
"i": ["u", "o", "k", "j"],
"o": ["i", "p", "l", "k"],
"p": ["o", "l"],
// Middle row - more fat finger mistakes due to touch
"a": ["q", "w", "s", "z"],
"s": ["a", "d", "w", "e", "z", "x"],
// High mistake area on mobile
"d": ["s", "f", "e", "r", "x", "c"],
"f": ["d", "g", "r", "t", "c", "v"],
"g": ["f", "h", "t", "y", "v", "b"],
"h": ["g", "j", "y", "u", "b", "n"],
"j": ["h", "k", "u", "i", "n", "m"],
"k": ["j", "l", "i", "o", "m"],
"l": ["k", "o", "p"],
// Bottom row - spacebar interference more common on mobile
"z": ["a", "s", "x"],
"x": ["z", "c", "s", "d"],
"c": ["x", "v", "d", "f", " "],
// Space bar mistakes more common
"v": ["c", "b", "f", "g", " "],
"b": ["v", "n", "g", "h", " "],
"n": ["b", "m", "h", "j", " "],
"m": ["n", "j", "k", " "],
// Space bar - different pattern on mobile due to wider space key
" ": ["c", "v", "b", "n", "m", "x", "z"],
// Includes more bottom row keys
// Numbers often above letters on mobile, but closer spacing
"1": ["2", "q", "w"],
"2": ["1", "3", "q", "w", "e"],
"3": ["2", "4", "w", "e", "r"],
"4": ["3", "5", "e", "r", "t"],
"5": ["4", "6", "r", "t", "y"],
"6": ["5", "7", "t", "y", "u"],
"7": ["6", "8", "y", "u", "i"],
"8": ["7", "9", "u", "i", "o"],
"9": ["8", "0", "i", "o", "p"],
"0": ["9", "o", "p"]
};
const QWERTY_ADJACENT = DESKTOP_ADJACENT;
function getAdjacentKeys(keyboardMode) {
return keyboardMode === "mobile" ? MOBILE_ADJACENT : DESKTOP_ADJACENT;
}
const COMMON_WORDS = /* @__PURE__ */ new Set([
"the",
"and",
"for",
"are",
"but",
"not",
"you",
"all",
"can",
"had",
"her",
"was",
"one",
"our",
"out",
"day",
"get",
"has",
"him",
"his",
"how",
"man",
"new",
"now",
"old",
"see",
"two",
"way",
"who",
"boy",
"did",
"its",
"let",
"put",
"say",
"she",
"too",
"use",
"that",
"with",
"have",
"this",
"will",
"your",
"from",
"they",
"know",
"want",
"been",
"good",
"much",
"some",
"time",
"very",
"when",
"come",
"here",
"just",
"like",
"long",
"make",
"many",
"over",
"such",
"take",
"than",
"them",
"well",
"were"
]);
const COMMON_TYPOS = {
"the": "teh",
"and": "adn",
"for": "fro",
"you": "yuo",
"that": "taht",
"this": "tihs",
"with": "wiht",
"have": "ahve",
"from": "form",
"they": "thye",
"been": "bene",
"than": "htan",
"what": "waht",
"your": "yuor",
"when": "wehn",
"there": "tehre",
"their": "thier",
"would": "woudl",
"could": "coudl",
"should": "shoudl",
"through": "trhough",
"because": "becasue",
"before": "beofre",
"after": "aftre",
"where": "whree",
"which": "whihc",
"between": "betwene",
"different": "differnet",
"important": "importnat",
"example": "exmaple",
"without": "withuot",
"another": "antoher",
"development": "developement",
"environment": "enviroment",
"government": "goverment",
"management": "managment",
"information": "infromation",
"available": "availabe",
"business": "buisness",
"complete": "compelte",
"language": "langauge",
"experience": "experiance",
"position": "postion",
"question": "quesiton",
"remember": "remeber",
"separate": "seperate",
"something": "somehting",
"together": "togehter",
"understand": "udnerstand"
};
const SPECIAL_CHARS = /* @__PURE__ */ new Set([
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"-",
"_",
"+",
"=",
"[",
"]",
"{",
"}",
"\\",
"|",
";",
":",
"'",
'"',
",",
".",
"<",
">",
"/",
"?",
"`",
"~"
]);
const SHIFT_CHARS = /* @__PURE__ */ new Set([
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"_",
"+",
"{",
"}",
"|",
":",
'"',
"<",
">",
"?",
"~"
]);
const NUMBER_CHARS = /* @__PURE__ */ new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
const SYMBOL_COMPLEXITY = {
// Simple punctuation
".": 1,
",": 1,
"?": 1,
"!": 1,
";": 1,
":": 1,
"'": 1,
'"': 1,
// Medium complexity
"-": 2,
"_": 2,
"(": 2,
")": 2,
"[": 2,
"]": 2,
"/": 2,
// Complex symbols requiring precise finger movement
"@": 3,
"#": 3,
"$": 3,
"%": 3,
"^": 3,
"&": 3,
"*": 3,
"+": 3,
"=": 3,
"{": 3,
"}": 3,
"\\": 3,
"|": 3,
"`": 3,
"~": 3,
"<": 3,
">": 3
};
const SENTENCE_ENDINGS = /* @__PURE__ */ new Set([".", "!", "?"]);
const CLAUSE_SEPARATORS = /* @__PURE__ */ new Set([",", ";", ":"]);
const LINE_BREAK_CHARS = /* @__PURE__ */ new Set(["\n", "\r\n", "\r"]);
const DEFAULT_CONFIG = {
speed: TIMING_CONSTANTS.BASE_SPEED,
speedVariation: TIMING_CONSTANTS.SPEED_VARIATION,
mistakeFrequency: BEHAVIOR_RATES.MISTAKE_FREQUENCY,
mistakeTypes: {
adjacent: true,
random: false,
doubleChar: true,
commonTypos: true
},
fatigueEffect: true,
concentrationLapses: true,
overcorrection: true,
debug: false,
sentencePause: TIMING_CONSTANTS.SENTENCE_PAUSE,
wordPause: TIMING_CONSTANTS.WORD_SPACE,
thinkingPause: TIMING_CONSTANTS.THINKING_PAUSE,
minCharDelay: TIMING_CONSTANTS.MIN_CHAR_DELAY,
backspaceSpeed: TIMING_CONSTANTS.BACKSPACE_SPEED,
realizationDelay: TIMING_CONSTANTS.REALIZATION_DELAY,
correctionPause: TIMING_CONSTANTS.CORRECTION_PAUSE,
// Keyboard simulation defaults
keyboardMode: "mobile"
};
const MOBILE_LAYOUT = {
views: {
letters: [
"q",
"w",
"e",
"r",
"t",
"y",
"u",
"i",
"o",
"p",
"a",
"s",
"d",
"f",
"g",
"h",
"j",
"k",
"l",
"z",
"x",
"c",
"v",
"b",
"n",
"m"
],
numbers: [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"-",
"/",
":",
";",
"(",
")",
"$",
"&",
"@",
'"',
".",
",",
"?",
"!",
"'",
"[",
"]",
"{",
"}",
"#",
"%",
"^",
"*",
"+",
"=",
"_",
"\\",
"|",
"~",
"<",
">",
"€",
"£",
"¥",
"•",
"..."
],
symbols: [
"[",
"]",
"{",
"}",
"#",
"%",
"^",
"*",
"+",
"=",
"_",
"\\",
"|",
"~",
"<",
">",
"€",
"£",
"¥",
"•",
".",
",",
"?",
"!",
"'",
'"',
"/",
":",
";",
"(",
")",
"$",
"&",
"@",
"`",
"§",
"¿",
"¡",
"«",
"»",
"°",
"†",
"‡",
"…",
"‰",
"′",
"″",
"‹",
"›"
],
emoji: [
"😀",
"😃",
"😄",
"😁",
"😆",
"😅",
"🤣",
"😂",
"🙂",
"🙃",
"😉",
"😊",
"😇",
"🥰",
"😍",
"🤩",
"😘",
"😗",
"☺",
"😚",
"😙",
"🥲",
"😋",
"😛",
"😜",
"🤪",
"😝",
"🤑",
"🤗",
"🤭"
]
},
viewSwitchers: {
toNumbers: "123",
toSymbols: "#+=",
toLetters: "ABC",
toEmoji: "😀"
},
modifiers: {
shift: "⇧",
caps: "CAPS",
space: "space",
enter: "return",
backspace: "⌫"
},
keyDurations: {
letter: 80,
// Fast letter typing
number: 90,
// Slightly slower for numbers
symbol: 100,
// Slower for symbols
modifier: 120,
// Shift/caps key press
viewSwitch: 110,
// View switching (123, ABC, etc.)
space: 75,
// Space bar
enter: 90,
// Return key
backspace: 110
// Backspace key
}
};
({
...MOBILE_LAYOUT
});
({
...MOBILE_LAYOUT
});
const MOBILE_CHARACTER_TO_VIEW = {
// Letters (lowercase and uppercase)
...Object.fromEntries("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => [c, "letters"])),
// Numbers and basic symbols on number view
"1": "numbers",
"2": "numbers",
"3": "numbers",
"4": "numbers",
"5": "numbers",
"6": "numbers",
"7": "numbers",
"8": "numbers",
"9": "numbers",
"0": "numbers",
"-": "numbers",
"/": "numbers",
":": "numbers",
";": "numbers",
"(": "numbers",
")": "numbers",
"$": "numbers",
"&": "numbers",
"@": "numbers",
'"': "numbers",
".": "numbers",
",": "numbers",
"?": "numbers",
"!": "numbers",
"'": "numbers",
// Complex symbols on symbol view
"[": "symbols",
"]": "symbols",
"{": "symbols",
"}": "symbols",
"#": "symbols",
"%": "symbols",
"^": "symbols",
"*": "symbols",
"+": "symbols",
"=": "symbols",
"_": "symbols",
"\\": "symbols",
"|": "symbols",
"~": "symbols",
"<": "symbols",
">": "symbols",
"€": "symbols",
"£": "symbols",
"¥": "symbols",
"•": "symbols",
"`": "symbols",
"§": "symbols",
"¿": "symbols",
"¡": "symbols",
"«": "symbols",
"»": "symbols",
"°": "symbols",
"†": "symbols",
"‡": "symbols",
"…": "symbols",
"‰": "symbols",
"′": "symbols",
"″": "symbols",
"‹": "symbols",
"›": "symbols"
};
const DESKTOP_QWERTY_LAYOUT = {
views: {
letters: [
"q",
"w",
"e",
"r",
"t",
"y",
"u",
"i",
"o",
"p",
"a",
"s",
"d",
"f",
"g",
"h",
"j",
"k",
"l",
"z",
"x",
"c",
"v",
"b",
"n",
"m"
],
numbers: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
symbols: [
"`",
"~",
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"-",
"_",
"+",
"=",
"[",
"]",
"{",
"}",
"\\",
"|",
";",
":",
"'",
'"',
",",
".",
"<",
">",
"/",
"?"
]
},
viewSwitchers: {
toNumbers: "numbers",
// No view switching on desktop
toSymbols: "symbols",
// Symbols accessed via shift
toLetters: "letters"
},
modifiers: {
shift: "shift",
caps: "caps lock",
space: "space",
enter: "enter",
backspace: "backspace"
},
keyDurations: {
letter: 70,
// Fast letter typing on desktop
number: 85,
// Numbers require reaching up
symbol: 95,
// Symbols often need shift
modifier: 90,
// Modifier key press
viewSwitch: 0,
// No view switching on desktop
space: 60,
// Space bar
enter: 80,
// Enter key
backspace: 100
// Backspace key
}
};
const DESKTOP_KEY_MAPPING = {
// Letters (lowercase)
"a": ["a", false],
"b": ["b", false],
"c": ["c", false],
"d": ["d", false],
"e": ["e", false],
"f": ["f", false],
"g": ["g", false],
"h": ["h", false],
"i": ["i", false],
"j": ["j", false],
"k": ["k", false],
"l": ["l", false],
"m": ["m", false],
"n": ["n", false],
"o": ["o", false],
"p": ["p", false],
"q": ["q", false],
"r": ["r", false],
"s": ["s", false],
"t": ["t", false],
"u": ["u", false],
"v": ["v", false],
"w": ["w", false],
"x": ["x", false],
"y": ["y", false],
"z": ["z", false],
// Letters (uppercase) - same key + shift
"A": ["a", true],
"B": ["b", true],
"C": ["c", true],
"D": ["d", true],
"E": ["e", true],
"F": ["f", true],
"G": ["g", true],
"H": ["h", true],
"I": ["i", true],
"J": ["j", true],
"K": ["k", true],
"L": ["l", true],
"M": ["m", true],
"N": ["n", true],
"O": ["o", true],
"P": ["p", true],
"Q": ["q", true],
"R": ["r", true],
"S": ["s", true],
"T": ["t", true],
"U": ["u", true],
"V": ["v", true],
"W": ["w", true],
"X": ["x", true],
"Y": ["y", true],
"Z": ["z", true],
// Numbers (no shift)
"1": ["1", false],
"2": ["2", false],
"3": ["3", false],
"4": ["4", false],
"5": ["5", false],
"6": ["6", false],
"7": ["7", false],
"8": ["8", false],
"9": ["9", false],
"0": ["0", false],
// Numbers with shift (top row symbols)
"!": ["1", true],
"@": ["2", true],
"#": ["3", true],
"$": ["4", true],
"%": ["5", true],
"^": ["6", true],
"&": ["7", true],
"*": ["8", true],
"(": ["9", true],
")": ["0", true],
// Other keys without shift
"`": ["`", false],
"-": ["-", false],
"=": ["=", false],
"[": ["[", false],
"]": ["]", false],
"\\": ["\\", false],
";": [";", false],
"'": ["'", false],
",": [",", false],
".": [".", false],
"/": ["/", false],
" ": ["space", false],
"\n": ["enter", false],
" ": ["tab", false],
// Other keys with shift
"~": ["`", true],
"_": ["-", true],
"+": ["=", true],
"{": ["[", true],
"}": ["]", true],
"|": ["\\", true],
":": [";", true],
'"': ["'", true],
"<": [",", true],
">": [".", true],
"?": ["/", true]
};
const MOBILE_TIMING_PROFILES = {
// Standard smartphone typing
MOBILE_CASUAL: {
name: "Mobile Casual",
description: "Average smartphone user, thumb typing",
baseMultiplier: 1,
keyDurations: {
letter: 120,
number: 140,
symbol: 160,
modifier: 180,
viewSwitch: 150,
space: 100,
enter: 130,
backspace: 150
},
viewSwitchDelay: 50,
consecutiveKeyBonus: 0.9,
complexSymbolPenalty: 40,
capsLockTransitionDelay: 60
},
// Fast mobile typist
MOBILE_FAST: {
name: "Mobile Fast",
description: "Experienced mobile user, swipe/predictive text habits",
baseMultiplier: 0.7,
keyDurations: {
letter: 80,
number: 95,
symbol: 110,
modifier: 120,
viewSwitch: 100,
space: 70,
enter: 90,
backspace: 110
},
viewSwitchDelay: 30,
consecutiveKeyBonus: 0.85,
complexSymbolPenalty: 25,
capsLockTransitionDelay: 40
},
// Slow/careful mobile typing
MOBILE_CAREFUL: {
name: "Mobile Careful",
description: "Deliberate mobile typing, hunt-and-peck style",
baseMultiplier: 1.5,
keyDurations: {
letter: 180,
number: 220,
symbol: 280,
modifier: 250,
viewSwitch: 200,
space: 150,
enter: 180,
backspace: 220
},
viewSwitchDelay: 80,
consecutiveKeyBonus: 0.95,
complexSymbolPenalty: 60,
capsLockTransitionDelay: 100
},
// Tablet typing (larger screen, different hand position)
TABLET: {
name: "Tablet",
description: "Tablet device, often landscape mode with more fingers",
baseMultiplier: 0.8,
keyDurations: {
letter: 90,
number: 110,
symbol: 130,
modifier: 140,
viewSwitch: 120,
space: 80,
enter: 100,
backspace: 130
},
viewSwitchDelay: 40,
consecutiveKeyBonus: 0.8,
// More fingers = better hand alternation
complexSymbolPenalty: 30,
capsLockTransitionDelay: 50
}
};
const DESKTOP_TIMING_PROFILES = {
// Standard desktop typing
DESKTOP_AVERAGE: {
name: "Desktop Average",
description: "Average desktop user, ~40 WPM",
baseMultiplier: 1,
keyDurations: {
letter: 80,
number: 100,
symbol: 120,
modifier: 90,
viewSwitch: 0,
// No view switching on desktop
space: 70,
enter: 90,
backspace: 100
},
viewSwitchDelay: 0,
consecutiveKeyBonus: 0.85,
// Good hand alternation on QWERTY
complexSymbolPenalty: 30,
capsLockTransitionDelay: 50
},
// Fast desktop typist
DESKTOP_FAST: {
name: "Desktop Fast",
description: "Experienced typist, ~70+ WPM, touch typing",
baseMultiplier: 0.6,
keyDurations: {
letter: 50,
number: 65,
symbol: 80,
modifier: 60,
viewSwitch: 0,
space: 45,
enter: 60,
backspace: 70
},
viewSwitchDelay: 0,
consecutiveKeyBonus: 0.8,
complexSymbolPenalty: 15,
capsLockTransitionDelay: 25
},
// Programming-focused typing
DESKTOP_PROGRAMMER: {
name: "Desktop Programmer",
description: "Developer typing, frequent symbols and modifiers",
baseMultiplier: 0.7,
keyDurations: {
letter: 60,
number: 70,
symbol: 75,
// Programmers are fast with symbols
modifier: 65,
viewSwitch: 0,
space: 50,
enter: 70,
backspace: 80
},
viewSwitchDelay: 0,
consecutiveKeyBonus: 0.8,
complexSymbolPenalty: 10,
// Less penalty for complex symbols
capsLockTransitionDelay: 30
},
// Hunt-and-peck desktop typing
DESKTOP_SLOW: {
name: "Desktop Slow",
description: "Hunt-and-peck typing, looking at keyboard",
baseMultiplier: 2,
keyDurations: {
letter: 160,
number: 200,
symbol: 250,
modifier: 180,
viewSwitch: 0,
space: 140,
enter: 160,
backspace: 180
},
viewSwitchDelay: 0,
consecutiveKeyBonus: 0.95,
// Less benefit from hand alternation
complexSymbolPenalty: 80,
capsLockTransitionDelay: 120
},
// Gaming keyboard (mechanical, fast response)
DESKTOP_GAMING: {
name: "Desktop Gaming",
description: "Mechanical keyboard, gaming-optimized typing",
baseMultiplier: 0.5,
keyDurations: {
letter: 40,
number: 50,
symbol: 60,
modifier: 45,
viewSwitch: 0,
space: 35,
enter: 50,
backspace: 60
},
viewSwitchDelay: 0,
consecutiveKeyBonus: 0.75,
complexSymbolPenalty: 10,
capsLockTransitionDelay: 20
}
};
function getDefaultTimingProfile(keyboardMode, userHint) {
if (keyboardMode === "mobile") {
if (userHint === "fast") return MOBILE_TIMING_PROFILES.MOBILE_FAST;
if (userHint === "slow" || userHint === "careful") return MOBILE_TIMING_PROFILES.MOBILE_CAREFUL;
if (userHint === "tablet") return MOBILE_TIMING_PROFILES.TABLET;
return MOBILE_TIMING_PROFILES.MOBILE_CASUAL;
} else {
if (userHint === "fast") return DESKTOP_TIMING_PROFILES.DESKTOP_FAST;
if (userHint === "slow") return DESKTOP_TIMING_PROFILES.DESKTOP_SLOW;
if (userHint === "programmer" || userHint === "developer") return DESKTOP_TIMING_PROFILES.DESKTOP_PROGRAMMER;
if (userHint === "gaming") return DESKTOP_TIMING_PROFILES.DESKTOP_GAMING;
return DESKTOP_TIMING_PROFILES.DESKTOP_AVERAGE;
}
}
function applyTimingProfile(layout, profile) {
return {
...layout,
keyDurations: {
...profile.keyDurations
}
};
}
function calculateContextualTiming(baseDelay, profile, context) {
let adjustedDelay = baseDelay * profile.baseMultiplier;
if (context.isConsecutiveSameHand) {
adjustedDelay *= profile.consecutiveKeyBonus;
}
if (context.isComplexSymbol) {
adjustedDelay += profile.complexSymbolPenalty;
}
if (context.isViewSwitch) {
adjustedDelay += profile.viewSwitchDelay;
}
if (context.isCapsLockTransition) {
adjustedDelay += profile.capsLockTransitionDelay;
}
return adjustedDelay;
}
class KeyboardAnalyzer {
constructor(config = {}) {
__publicField(this, "config");
__publicField(this, "layout");
__publicField(this, "state");
__publicField(this, "timingProfile");
this.config = {
keyboardMode: "mobile",
capsLockThreshold: 3,
useNaturalTiming: true,
debug: false,
...config
};
this.timingProfile = getDefaultTimingProfile(this.config.keyboardMode, this.config.typingSpeed);
const baseLayout = config.customLayout || (this.config.keyboardMode === "mobile" ? MOBILE_LAYOUT : DESKTOP_QWERTY_LAYOUT);
this.layout = applyTimingProfile(baseLayout, this.timingProfile);
this.state = {
currentView: "letters",
capsLockActive: false,
shiftActive: false,
recentCharacters: [],
mode: this.config.keyboardMode
};
this.debug("KeyboardAnalyzer initialized", {
config: this.config,
timingProfile: this.timingProfile.name,
layout: this.layout.viewSwitchers
});
}
/**
* Analyzes a character and returns the key sequence needed to type it
*/
analyzeCharacter(character, charIndex, fullText) {
this.debug(`Analyzing character: "${character}" at index ${charIndex}`);
this.updateRecentCharacters(character);
const capsLockInfo = this.analyzeCapsLockSequence(character, charIndex, fullText);
let keys = [];
if (this.config.keyboardMode === "mobile") {
keys = this.analyzeMobileCharacter(character, capsLockInfo);
} else {
keys = this.analyzeDesktopCharacter(character, capsLockInfo);
}
const totalDuration = keys.reduce((sum, key) => sum + key.duration, 0);
const sequence = {
character,
keys,
totalDuration,
usesCapsLock: capsLockInfo.isCapsLockSequence
};
this.debug(`Generated sequence for "${character}":`, sequence);
return sequence;
}
/**
* Analyzes mobile keyboard character input
*/
analyzeMobileCharacter(character, capsInfo) {
const keys = [];
let sequenceIndex = 0;
if (character === " ") {
return [{
key: this.layout.modifiers.space,
character,
type: "space",
keyboardView: this.state.currentView,
isCapsLock: false,
duration: this.layout.keyDurations.space,
sequenceIndex: 0,
sequenceLength: 1
}];
}
if (character === "\n") {
return [{
key: this.layout.modifiers.enter,
character,
type: "enter",
keyboardView: this.state.currentView,
isCapsLock: false,
duration: this.layout.keyDurations.enter,
sequenceIndex: 0,
sequenceLength: 1
}];
}
const targetView = this.getCharacterView(character);
if (this.state.currentView !== targetView) {
const viewSwitchKeys = this.getViewSwitchSequence(this.state.currentView, targetView);
viewSwitchKeys.forEach((viewKey) => {
keys.push({
...viewKey,
sequenceIndex: sequenceIndex++,
sequenceLength: 0
// Will be updated after we know total length
});
});
this.state.currentView = targetView;
}
if (character.match(/[A-Z]/)) {
if (capsInfo.isCapsLockSequence) {
if (capsInfo.isFirst) {
keys.push(this.createCapsKey(true, sequenceIndex++));
this.state.capsLockActive = true;
} else if (capsInfo.isLast) {
keys.push(this.createLetterKey(character.toLowerCase(), sequenceIndex++, capsInfo.isCapsLockSequence));
keys.push(this.createCapsKey(false, sequenceIndex++));
this.state.capsLockActive = false;
keys.forEach((key) => key.sequenceLength = keys.length);
return keys;
}
} else {
keys.push(this.createShiftKey(sequenceIndex++));
}
}
const mainKey = this.createCharacterKey(character, sequenceIndex++, capsInfo.isCapsLockSequence);
keys.push(mainKey);
keys.forEach((key) => key.sequenceLength = keys.length);
return keys;
}
/**
* Analyzes desktop keyboard character input
*/
analyzeDesktopCharacter(character, capsInfo) {
const keys = [];
let sequenceIndex = 0;
if (character === " ") {
return [{
key: "space",
character,
type: "space",
keyboardView: "letters",
isCapsLock: false,
duration: this.layout.keyDurations.space,
sequenceIndex: 0,
sequenceLength: 1
}];
}
if (character === "\n") {
return [{
key: "enter",
character,
type: "enter",
keyboardView: "letters",
isCapsLock: false,
duration: this.layout.keyDurations.enter,
sequenceIndex: 0,
sequenceLength: 1
}];
}
const mapping = DESKTOP_KEY_MAPPING[character];
if (!mapping) {
this.debug(`No desktop mapping found for character: "${character}"`);
return [{
key: character.toLowerCase(),
character,
type: "letter",
keyboardView: "letters",
isCapsLock: false,
duration: this.layout.keyDurations.letter,
sequenceIndex: 0,
sequenceLength: 1
}];
}
const [physicalKey, requiresShift] = mapping;
if (character.match(/[A-Z]/)) {
if (capsInfo.isCapsLockSequence) {
if (capsInfo.isFirst) {
keys.push({
key: "caps lock",
character,
type: "modifier",
keyboardView: "letters",
isCapsLock: true,
duration: this.layout.keyDurations.modifier,
sequenceIndex: sequenceIndex++,
sequenceLength: 0
});
} else if (capsInfo.isLast) {
keys.push({
key: physicalKey,
character,
type: "letter",
keyboardView: "letters",
isCapsLock: true,
duration: this.layout.keyDurations.letter,
sequenceIndex: sequenceIndex++,
sequenceLength: 0
});
keys.push({
key: "caps lock",
character,
type: "modifier",
keyboardView: "letters",
isCapsLock: true,
duration: this.layout.keyDurations.modifier,
sequenceIndex: sequenceIndex++,
sequenceLength: 0
});
keys.forEach((key) => key.sequenceLength = keys.length);
return keys;
}
} else if (requiresShift) {
keys.push({
key: "shift",
character,
type: "modifier",
keyboardView: "letters",
isCapsLock: false,
duration: this.layout.keyDurations.modifier,
sequenceIndex: sequenceIndex++,
sequenceLength: 0
});
}
} else if (requiresShift) {
keys.push({
key: "shift",
character,
type: "modifier",
keyboardView: "symbols",
isCapsLock: false,
duration: this.layout.keyDurations.modifier,
sequenceIndex: sequenceIndex++,
sequenceLength: 0
});
}
const keyType = character.match(/[a-zA-Z]/) ? "letter" : character.match(/[0-9]/) ? "number" : "symbol";
keys.push({
key: physicalKey,
character,
type: keyType,
keyboardView: keyType === "letter" ? "letters" : keyType === "number" ? "numbers" : "symbols",
isCapsLock: capsInfo.isCapsLockSequence,
duration: this.layout.keyDurations[keyType],
sequenceIndex: sequenceIndex++,
sequenceLength: 0
});
keys.forEach((key) => key.sequenceLength = keys.length);
return keys;
}
/**
* Determines which keyboard view a character belongs to (mobile)
*/
getCharacterView(character) {
if (this.config.keyboardMode === "desktop") {
return "letters";
}
return MOBILE_CHARACTER_TO_VIEW[character] || "letters";
}
/**
* Gets the sequence of view switch keys needed
*/
getViewSwitchSequence(from, to) {
if (from === to) return [];
const keys = [];
switch (to) {
case "numbers":
keys.push({
key: this.layout.viewSwitchers.toNumbers,
character: "",
type: "view-switch",
keyboardView: from,
isCapsLock: false,
duration: this.layout.keyDurations.viewSwitch,
sequenceIndex: 0,
sequenceLength: 0
});
break;
case "symbols":
if (from === "letters") {
keys.push({
key: this.layout.viewSwitchers.toNumbers,
character: "",
type: "view-switch",
keyboardView: from,
isCapsLock: false,
duration: this.layout.keyDurations.viewSwitch,
sequenceIndex: 0,
sequenceLength: 0
});
}
keys.push({
key: this.layout.viewSwitchers.toSymbols,
character: "",
type: "view-switch",
keyboardView: "numbers",
isCapsLock: false,
duration: this.layout.keyDurations.viewSwitch,
sequenceIndex: 0,
sequenceLength: 0
});
break;
case "letters":
keys.push({
key: this.layout.viewSwitchers.toLetters,
character: "",
type: "view-switch",
keyboardView: from,
isCapsLock: false,
duration: this.layout.keyDurations.viewSwitch,
sequenceIndex: 0,
sequenceLength: 0
});
break;
}
return keys;
}
/**
* Creates a caps lock key press
*/
createCapsKey(turningOn, sequenceIndex) {
const baseDuration = this.layout.keyDurations.modifier;
const contextualDuration = calculateContextualTiming(baseDuration, this.timingProfile, {
isCapsLockTransition: true
});
return {
key: this.layout.modifiers.caps,
character: "",
type: "modifier",
keyboardView: this.state.currentView,
isCapsLock: true,
duration: turningOn ? contextualDuration + 20 : contextualDuration,
// Slightly longer for turning on
sequenceIndex,
sequenceLength: 0
};
}
/**
* Creates a shift key press
*/
createShiftKey(sequenceIndex) {
return {
key: this.layout.modifiers.shift,
character: "",
type: "modifier",
keyboardView: this.state.currentView,
isCapsLock: false,
duration: this.layout.keyDurations.modifier,
sequenceIndex,
sequenceLength: 0
};
}
/**
* Creates a letter key press
*/
createLetterKey(letter, sequenceIndex, isCapsLock) {
return {
key: letter,
character: letter,
type: "letter",
keyboardView: "letters",
isCapsLock,
duration: this.layout.keyDurations.letter,
sequenceIndex,
sequenceLength: 0
};
}
/**
* Creates a character key press
*/
createCharacterKey(character, sequenceIndex, isCapsLock) {
const key = character.toLowerCase();
const type = character.match(/[a-zA-Z]/) ? "letter" : character.match(/[0-9]/) ? "number" : "symbol";
const baseDuration = this.layout.keyDurations[type];
const isComplexSymbol = type === "symbol" && this.isComplexSymbol(character);
const contextualDuration = calculateContextualTiming(baseDuration, this.timingProfile, {
isComplexSymbol
});
return {
key,
character,
type,
keyboardView: this.state.currentView,
isCapsLock,
duration: contextualDuration,
sequenceIndex,
sequenceLength: 0
};
}
/**
* Updates recent characters for caps lock detection
*/
updateRecentCharacters(character) {
this.state.recentCharacters.push(character);
if (this.state.recentCharacters.length > 10) {
this.state.recentCharacters.shift();
}
}
/**
* Analyzes whether character is part of a caps lock sequence
* Uses the same logic as the original TypingEngine
*/
analyzeCapsLockSequence(character, charIndex, fullText) {
if (!character.match(/[A-Z]/)) {
return { isCapsLockSequence: false, isFirst: false, isLast: false };
}
let sequenceStart = charIndex;
let sequenceEnd = charIndex;
while (sequenceStart > 0) {
const prevChar = fullText[sequenceStart - 1];
if (prevChar.match(/[A-Z]/) || prevChar === " ") {
sequenceStart--;
} else {
break;
}
}
while (sequenceEnd < fullText.length - 1) {
const nextChar = fullText[sequenceEnd + 1];
if (nextChar.match(/[A-Z]/) || nextChar === " ") {
sequenceEnd++;
} else {
break;
}
}
let capitalCount = 0;
for (let i = sequenceStart; i <= sequenceEnd; i++) {
if (fullText[i].match(/[A-Z]/)) {
capitalCount++;
}
}
const isCapsLockSequence = capitalCount >= this.config.capsLockThreshold;
let firstCapitalIndex = sequenceStart;
while (firstCapitalIndex <= sequenceEnd && !fullText[firstCapitalIndex].match(/[A-Z]/)) {
firstCapitalIndex++;
}
let lastCapitalIndex = sequenceEnd;
while (lastCapitalIndex >= sequenceStart && !fullText[lastCapitalIndex].match(/[A-Z]/)) {
lastCapitalIndex--;
}
return {
isCapsLockSequence,
isFirst: isCapsLockSequence && charIndex === firstCapitalIndex,
isLast: isCapsLockSequence && charIndex === lastCapitalIndex
};
}
/**
* Check if a symbol is considered complex (requires more precise movement)
*/
isComplexSymbol(character) {
const complexSymbols = /* @__PURE__ */ new Set(["@", "#", "$", "%", "^", "&", "*", "+", "=", "{", "}", "\\", "|", "`", "~", "<", ">"]);
return complexSymbols.has(character);
}
/**
* Debug logging
*/
debug(message, data) {
if (this.config.debug) {
console.log(`[KeyboardAnalyzer] ${message}`, data || "");
}
}
/**
* Reset keyboard state (useful for testing)
*/
resetState() {
this.state = {
currentView: "letters",
capsLockActive: false,
shiftActive: false,
recentCharacters: [],
mode: this.config.keyboardMode
};
}
/**
* Analyzes a backspace key press and returns the key sequence
*/
analyzeBackspace() {
this.debug("Analyzing backspace key");
const backspaceKey = {
key: this.config.keyboardMode === "mobile" ? this.layout.modifiers.backspace : "backspace",
character: "\b",
// Backspace character
type: "backspace",
keyboardView: this.state.currentView,
isCapsLock: false,
duration: this.layout.keyDurations.backspace || this.layout.keyDurations.modifier || 120,
sequenceIndex: 0,
sequenceLength: 1
};
const sequence = {
character: "\b",
keys: [backspaceKey],
totalDuration: backspaceKey.duration,
usesCapsLock: false
};
this.debug("Generated backspace sequence:", sequence);
return sequence;
}
/**
* Get current keyboard state (useful for debugging)
*/
getState() {
return { ...this.state };
}
}
class TypingEngine {
constructor(text, config = {}) {
__publicField(this, "config");
__publicField(this, "text");
__publicField(this, "currentIndex", 0);
__publicField(this, "displayText", "");
__publicField(this, "state", "idle");
__publicField(this, "timeoutId", null);
__publicField(this, "keyTimeouts", /* @__PURE__ */ new Set());
// Track all keyboard timing timeouts
__publicField(this, "stats");
__publicField(this, "events", []);
__publicField(this, "mistakes", []);
__publicField(this, "correctionQueue", []);
__publicField(this, "isCorrectingMistake", false);
__publicField(this, "charactersTyped", 0);
__publicField(this, "fatigueLevel", 0);
__publicField(this, "pauseStartTime", 0);
__publicField(this, "totalPausedTime", 0);
// Keyboard simulation - always enabled now
__publicField(this, "keyboardAnalyzer");
// Event callbacks
__publicField(this, "onStateChange");
__publicField(this, "onCharacter");
__publicField(this, "onMistake");
__publicField(this, "onBackspace");
__publicField(this, "onComplete");
__publicField(this, "onProgress");
__publicField(this, "onKey");
this.text = text ?? "";
this.config = { ...DEFAULT_CONFIG, ...config };
this.stats = this.initializeStats();
if (this.config.onKey) {
this.onKey = this.config.onKey;
}
this.keyboardAnalyzer = new KeyboardAnalyzer({
keyboardMode: this.config.keyboardMode || "mobile",
capsLockThreshold: TIMING_CONSTANTS.CAPS_SEQUENCE_THRESHOLD,
useNaturalTiming: true,
// Always use natural keyboard timing
debug: this.config.debug
});
}
debug(...args) {
if (this.config.debug) {
console.log(...args);
}
}
safeCallback(callback, ...args) {
if (callback) {
try {
callback(...args);
} catch (error) {
if (this.config.debug) {
console.warn("Callback error:", error);
}
}
}
}
initializeStats() {
return {
totalCharacters: this.text.length,
charactersTyped: 0,
mistakesMade: 0,
mistakesCorrected: 0,
startTime: 0,
currentWPM: 0,
averageCharDelay: 0,
totalDuration: 0
};
}
start() {
if (this.state === "typing" || this.state === "correcting") return;
this.state = "typing";
const now = Date.now();
if (this.stats.startTime === 0) {
this.stats.startTime = now;
}
this.safeCallback(this.onStateChange, "typing");
this.scheduleNextCharacter();
}
stop() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
this.keyTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
this.keyTimeouts.clear();
for (const mistake of this.mistakes) {
if (!mistake.corrected) {
mistake.corrected = true;
}
}
this.correctionQueue = [];
this.isCorrectingMistake = false;
this.state = "idle";
this.safeCallback(this.onStateChange, "idle");
}
pause() {
if (this.state === "typing" || this.state === "correcting") {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
this.pauseStartTime = Date.now();
this.state = "paused";
this.safeCallback(this.onStateChange, "paused");
}
}
resume() {
if (this.state === "paused") {
if (this.pauseStartTime > 0) {
this.totalPausedTime += Date.now() - this.pauseStartTime;
this.pauseStartTime = 0;
}
this.state = "typing";
this.safeCallback(this.onStateChange, "typing");
this.scheduleNextCharacter();
}
}
skip() {
this.currentIndex = this.text.length;
this.displayText = this.text;
this.state = "completed";
this.stats.endTime = Date.now();
this.safeCallback(this.onStateChange, "completed");
this.safeCallback(this.onComplete);
}
reset() {
this.stop();
this.currentIndex = 0;
this.displayText = "";
this.charactersTyped = 0;
this.fatigueLevel = 0;
this.mistakes = [];
this.correctionQueue = [];
this.isCorrectingMistake = false;
this.events = [];
this.pauseStartTime = 0;
this.totalPausedTime = 0;
this.stats = this.initializeStats();
this.state = "idle";
this.safeCallback(this.onStateChange, "idle");
}
scheduleNextCharacter() {
if (this.text.length === 0) {
this.completeTyping();
return;
}
if (this.currentIndex >= this.text.length) {
const hasUncorrectedMistakes = this.mistakes.some((m) => !m.corrected);
const hasPendingCorrections = this.correctionQueue.length > 0;
if (hasUncorrectedMistakes || hasPendingCorrections) {
this.debug(`📝 Reached end but have ${this.mistakes.filter((m) => !m.corrected).length} uncorrected mistakes and ${this.correctionQueue.length} queued corrections`);
this.processNextCorrection();
return;
}
if (this.keyTimeouts.size > 0) {
this.debug(`⏳ Reached end but waiting for ${this.keyTimeouts.size} key timeouts to complete`);
this.timeoutId = window.setTimeout(() => {
this.scheduleNextCharacter();
}, 50);
return;
}
this.completeTyping();
return;
}
const char = this.text[this.currentIndex];
const delay = this.calculateCharacterDelayWithKeyboard(char);
if (this.currentIndex > 0 && this.shouldHaveConcentrationLapse()) {
this.state = "thinking";
this.safeCallback(this.onStateChange, "thinking");
this.timeoutId = window.setTimeout(() => {
this.state = "typing";
this.safeCallback(this.onStateChange, "typing");
this.scheduleNextCharacter();
}, TIMING_CONSTANTS.CONCENTRATION_PAUSE);
return;
}
const shouldMakeMistake = this.shouldMakeMistake(char);
if (delay < 30) {
requestAnimationFrame(() => {
if (shouldMakeMistake) {
this.makeMistake(char);
} else {
this.typeCharacter(char);
}
});
} else {
this.timeoutId = window.setTimeout(() => {
if (shouldMakeMistake) {
this.makeMistake(char);
} else {
this.typeCharacter(char);
}
}, delay);
}
}
/**
* Calculate character delay using natural keyboard timing
* This revolutionary approach replaces artificial delays with realistic key sequences
*/
calculateCharacterDelayWithKeyboard(char) {
const keySequence = this.keyboardAnalyzer.analyzeCharacter(char, this.currentIndex, this.text);
const speedMultiplier = this.config.speed / 80;
const scaledKeys = keySequence.keys.map((keyInfo) => ({
...keyInfo,
duration: Math.round(keyInfo.duration * speedMultiplier)
}));
const scaledTotalDuration = scaledKeys.reduce((sum, key) => sum + key.duration, 0);
this.debug(`🎹 Natural timing for "${char}": ${scaledKeys.length} keys, ${scaledTotalDuration}ms total (speed: ${this.config.speed}ms, multiplier: ${speedMultiplier.toFixed(2)})`);
let cumulativeDelay = 0;
scaledKeys.forEach((keyInfo) => {
const keyTimeoutId = window.setTimeout(() => {
this.safeCallback(this.onKey, keyInfo);
this.debug(`🔑 Key press: "${keyInfo.key}" (${keyInfo.type}) - ${keyInfo.duration}ms`);
this.keyTimeouts.delete(keyTimeoutId);
}, cumulativeDelay);
this.keyTimeouts.add(keyTimeoutId);
cumulativeDelay += keyInfo.duration;
});
let totalDelay = scaledTotalDuration;
const variation = (Math.random() - 0.5) * 2 * this.config.speedVariation;
totalDelay += variation;
if (this.config.fatigueEffect) {
totalDelay += this.fatigueLevel;
this.fatigueLevel += TIMING_CONSTANTS.FATIGUE_INCREMENT;
}
if (Math.random() < BEHAVIOR_RATES.BURST_TYPING) {
totalDelay *= TIMING_CONSTANTS.BURST_SPEED_MULTIPLIER;
}
if (SENTENCE_ENDINGS.has(char)) {
totalDelay += this.config.sentencePause;
} else if (CLAUSE_SEPARATORS.has(char)) {
totalDelay += TIMING_CONSTANTS.COMMA_PAUSE;
} else if (LINE_BREAK_CHARS.has(char)) {
totalDelay += TIMING_CONSTANTS.LINE_BREAK;
} else if (char === " ") {
totalDelay += this.config.wordPause;
const nextWord = this.getNextWord();
if (nextWord && this.isComplexWord(nextWord)) {
totalDelay += this.config.thinkingPause;
}
}
return Math.max(this.config.minCharDelay, totalDelay);
}
shouldMakeMistake(char) {
if (char === " " || LINE_BREAK_CHARS.has(char) || this.currentIndex === 0) return false;
let mistakeChance = this.config.mistakeFrequency;
if (NUMBER_CHARS.has(char)) {
mistakeChance *= 1.5;
}
if (SHIFT_CHARS.has(char)) {
mistakeChance *= 1.2;
}
if (SYMBOL_COMPLEXITY[char] && SYMBOL_COMPLEXITY[char] >= 3) {
mistakeChance *= 1.3;
}
if (SPECIAL_CHARS.has(char) && (!SYMBOL_COMPLEXITY[char] || SYMBOL_COMPLEXITY[char] === 1)) {
mistakeChance *= 0.5;
}
if (this.shouldMakeLookAheadMistake()) {
mistakeChance *= 2;
}
return Math.random() < mistakeChance;
}
makeMistake(originalChar) {
const mistakeType = this.selectMistakeType(originalChar);
const mistakeChar = this.generateMistakeChar(originalChar, mistakeType);
if (!mistakeChar) {
this.typeCharacter(originalChar);
return;
}
const mistake = {
type: mistakeType,
originalChar,
mistakeChar,
position: this.currentIndex,
corrected: false,
realizationTime: this.config.realizationDelay + Math.random() * 150
};
this.mistakes.push(mistake);
this.stats.mistakesMade++;
this.debug(`🔴 MISTAKE: "${originalChar}" → "${mistakeChar}" (${mistakeType}) at pos ${this.currentIndex}`);
this.displayText += mistakeChar;
this.currentIndex++;
this.charactersTyped++;
this.stats.charactersTyped = this.charactersTyped;
this.recordEvent({
type: "mistake",
position: mistake.position,
timestamp: Date.now(),
char: mistakeChar,
mistake
});
this.safeCallback(this.onCharacter, mistakeChar, this.currentIndex - mistakeChar.length);
this.safeCallback(this.onMistake, mistake);
this.updateProgress();
const realizationTime = Math.max(mistake.realizationTime, 200);
this.debug(`📝 Adding "${mistakeChar}" to correction queue (will correct in ${realizationTime}ms)`);
this.timeoutId = window.setTimeout(() => {
if (!mistake.corrected) {
this.correctionQueue.push(mistake);
this.processNextCorrection();
}
}, realizationTime);
}
processNextCorrection() {
if (this.isCorrectingMistake || this.correctionQueue.length === 0) {
return;
}
const mistake = this.correctionQueue.pop();
if (mistake.corrected) {
this.processNextCorrection();
return;
}
this.debug(`🔧 Processing correction for "${mistake.mistakeChar}" → "${mistake.originalChar}"`);
this.correctMistake(mistake);
}
correctMistake(mistake) {
this.isCorrectingMistake = true;
this.state = "correcting";
this.safeCallback(this.onStateChange, "correcting");
this.debug(`🔧 Correcting mistake: "${mistake.mistakeChar}" → should be "${mistake.originalChar}"`);
this.debug(`📍 Current text: "${this.displayText}", current position: ${this.currentIndex}, mistake position: ${mistake.position}`);
const targetPosition = mistake.position;
const currentTextLength = this.displayText.length;
const charsToDelete = currentTextLength - targetPosition;
this.debug(`🔄 Need to delete ${charsToDelete} chars to get back to position ${targetPosition}`);
if (charsToDelete <= 0) {
this.debug(`⚠️ Already at correct position, marking as corrected`);
this.finishCorrection(mistake);
return;
}
this.performBackspaceSequence(mistake, charsToDelete, targetPosition);
}
performBackspaceSequence(mistake, charsToDelete, targetPosition) {
let deletedCount = 0;
const backspaceDelay = this.config.backspaceSpeed;
const performBackspace = () => {
if (deletedCount < charsToDelete && this.displayText.length > targetPosition) {
this.displayText = this.displayText.slice(