touch-typing / index.html
ariG23498's picture
ariG23498 HF Staff
Update index.html
ad41710 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bayesian Touch Typing Tutor - Learn Smarter, Type Faster</title>
<style>
:root {
--bg:#0f111a;
--card:#1f2235;
--radius:16px;
--primary:#7c5aff;
--success:#4ade80;
--error:#ef4444;
font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
}
*{box-sizing:border-box;}
body{margin:0; background:linear-gradient(135deg,#0f111a,#1f2235); color:#e8ebf7; min-height:100vh;}
/* Header styles */
.header {
background: rgba(31, 34, 53, 0.8);
backdrop-filter: blur(10px);
padding: 24px;
border-bottom: 1px solid rgba(255,255,255,0.1);
position: sticky;
top: 0;
z-index: 100;
}
.hero-section {
text-align: center;
padding: 40px 20px;
background: linear-gradient(135deg, rgba(124, 90, 255, 0.1), rgba(74, 222, 128, 0.1));
margin-bottom: 24px;
}
.hero-title {
font-size: 3rem;
margin: 0 0 16px 0;
background: linear-gradient(135deg, #7c5aff, #4ade80);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 1.2rem;
color: #aaa;
margin-bottom: 32px;
}
.benefit-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
max-width: 900px;
margin: 0 auto 40px;
}
.benefit-card {
background: rgba(31, 34, 53, 0.6);
padding: 24px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.1);
transition: all 0.3s;
}
.benefit-card:hover {
transform: translateY(-4px);
border-color: var(--primary);
box-shadow: 0 8px 24px rgba(124, 90, 255, 0.2);
}
.benefit-icon {
font-size: 2rem;
margin-bottom: 12px;
}
.benefit-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--primary);
}
.benefit-desc {
font-size: 0.9rem;
color: #aaa;
line-height: 1.5;
}
.container{max-width:1100px;margin:0 auto;padding:24px;display:grid;gap:24px;grid-template-columns:1fr 1fr;}
.card{background:var(--card);padding:24px;border-radius:var(--radius);box-shadow:0 24px 60px -10px rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.05);}
h2{margin-top:0;font-size:1.3rem;display:flex;align-items:center;gap:8px;}
textarea{
width:100%;
background:#0f132d;
border:2px solid #2a2f55;
padding:16px;
border-radius:8px;
color:#fff;
font-family:'Fira Code', 'Courier New', monospace;
font-size:1.2rem;
line-height:1.8;
resize:none;
transition: all 0.3s;
}
input{
width:100%;
background:#0f132d;
border:2px solid #2a2f55;
padding:12px;
border-radius:8px;
color:#fff;
font-family:'Fira Code', 'Courier New', monospace;
resize:none;
transition: all 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(124, 90, 255, 0.2);
}
.typing-container {
background: #0f132d;
border-radius: 12px;
padding: 24px;
margin-top: 16px;
}
.target-section {
margin-bottom: 24px;
}
.target-display {
background: #1f2235;
padding: 20px;
border-radius: 8px;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 1.3rem;
line-height: 1.8;
color: #e8ebf7;
min-height: 80px;
border: 2px solid #2a2f55;
position: relative;
white-space: pre-wrap;
word-wrap: break-word;
}
.target-display .char {
position: relative;
transition: all 0.2s;
}
.target-display .typed {
color: #4ade80;
background: rgba(74, 222, 128, 0.1);
}
.target-display .error {
color: #ef4444;
background: rgba(239, 68, 68, 0.2);
animation: shake 0.3s;
}
.target-display .current {
background: rgba(124, 90, 255, 0.3);
box-shadow: 0 0 0 2px var(--primary);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
.typing-section {
position: relative;
}
.typing-overlay {
position: absolute;
top: 40px;
left: 0;
right: 0;
pointer-events: none;
padding: 16px;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 1.2rem;
line-height: 1.8;
color: transparent;
white-space: pre-wrap;
word-wrap: break-word;
}
.typing-stats {
display: flex;
gap: 24px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #2a2f55;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-label {
display: block;
font-size: 0.85rem;
color: #888;
margin-bottom: 4px;
}
.stat-value {
display: block;
font-size: 1.8rem;
font-weight: 700;
color: var(--primary);
}
button{
background:var(--primary);
border:none;
color:#fff;
padding:12px 20px;
border-radius:8px;
cursor:pointer;
font-weight:600;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(124, 90, 255, 0.4);
}
.keyboard{display:flex;flex-direction:column;gap:6px;margin-top:16px;}
.key-row{display:flex;justify-content:center;gap:6px;}
.key{
position:relative;
width:52px;
height:52px;
border-radius:8px;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
font-weight:700;
user-select:none;
border:2px solid #2a2f55;
transition: all 0.3s;
cursor: default;
}
.key:hover {
transform: scale(1.05);
}
.key-letter {
font-size: 1.2rem;
}
.key-stats {
font-size: 0.65rem;
color: #aaa;
margin-top: 2px;
}
.drill{
background:#0f132d;
padding:16px;
border-radius:8px;
font-family:'Fira Code', monospace;
font-size:1.1rem;
line-height:1.6;
border: 2px solid #2a2f55;
}
.explanation{
background:#0f132d;
padding:16px;
border-radius:8px;
font-size:0.9rem;
line-height:1.6;
}
.stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;}
.stat-box{
background:#0f132d;
padding:16px;
border-radius:8px;
border:1px solid #2a2f55;
transition: all 0.3s;
}
.stat-box:hover {
border-color: var(--primary);
}
.stat-key {
font-size: 1.4rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 8px;
}
.footer{grid-column:1/-1;text-align:center;font-size:0.8rem;margin-top:40px;color:#999;}
.highlight{
background:rgba(124, 90, 255, 0.2);
padding:4px 8px;
border-radius:6px;
font-weight:600;
color: var(--primary);
}
.error-flash {
animation: errorFlash 0.5s;
}
@keyframes errorFlash {
0% { background-color: rgba(239, 68, 68, 0.3); }
100% { background-color: transparent; }
}
.success-flash {
animation: successFlash 0.5s;
}
@keyframes successFlash {
0% { background-color: rgba(74, 222, 128, 0.3); }
100% { background-color: transparent; }
}
.progress-bar {
height: 4px;
background: #2a2f55;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--success));
transition: width 0.3s;
}
.info-box {
background: rgba(124, 90, 255, 0.1);
border: 1px solid rgba(124, 90, 255, 0.3);
padding: 12px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #2a2f55;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
margin-bottom: 8px;
z-index: 1000;
}
.key:hover .tooltip {
opacity: 1;
}
@media (max-width: 768px) {
.container { grid-template-columns: 1fr; }
.hero-title { font-size: 2rem; }
.key { width: 42px; height: 42px; }
}
</style>
</head>
<body>
<div class="header">
<div style="max-width:1100px;margin:0 auto;display:flex;gap:16px;flex-wrap:wrap;align-items:center;">
<div style="flex:1;min-width:280px;">
<h1 style="margin:0;font-size:1.5rem;">🧠 Bayesian Touch Typing Tutor</h1>
</div>
<div style="flex:1;min-width:280px;text-align:right;">
<button id="resetBtn">🔄 Reset All Stats</button>
</div>
</div>
</div>
<div class="hero-section">
<h1 class="hero-title">Learn to Type Smarter, Not Harder</h1>
<p class="hero-subtitle">Using advanced Bayesian statistics to create personalized typing exercises based on YOUR unique patterns</p>
<div class="benefit-cards">
<div class="benefit-card">
<div class="benefit-icon">🎯</div>
<div class="benefit-title">Adaptive Learning</div>
<div class="benefit-desc">The tutor learns which keys you struggle with and creates custom drills targeting your weak spots</div>
</div>
<div class="benefit-card">
<div class="benefit-icon">📊</div>
<div class="benefit-title">Uncertainty-Aware</div>
<div class="benefit-desc">Knows when it needs more data about a key before making strong recommendations</div>
</div>
<div class="benefit-card">
<div class="benefit-icon"></div>
<div class="benefit-title">Real-Time Feedback</div>
<div class="benefit-desc">Visual heatmap shows your error patterns instantly, with glow indicating confidence levels</div>
</div>
</div>
</div>
<div class="container">
<div class="card" style="grid-column: 1 / -1;">
<h2>📝 Typing Area</h2>
<div class="info-box">
💡 <strong>How it works:</strong> Type the target text below. Each keystroke updates your personal error model using Bayesian inference.
</div>
<div class="typing-container">
<div class="target-section">
<label style="font-weight:600;color:#7c5aff;margin-bottom:8px;display:block;">Target Text:</label>
<div class="target-display" id="targetDisplay"></div>
<div style="margin-top:12px;">
<input id="target" value="the quick brown fox jumps over the lazy dog" placeholder="Enter custom text to practice" style="font-size:0.9rem;">
<button id="editTargetBtn" style="margin-left:8px;padding:8px 16px;font-size:0.9rem;">✏️ Edit</button>
</div>
</div>
<div class="typing-section">
<label style="font-weight:600;color:#4ade80;margin-bottom:8px;display:block;">Your Typing:</label>
<div class="typing-overlay" id="typingOverlay"></div>
<textarea id="typed" rows="3" placeholder="Start typing the target text above..." spellcheck="false"></textarea>
</div>
<div class="typing-stats">
<div class="stat-item">
<span class="stat-label">WPM</span>
<span class="stat-value" id="wpm">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Accuracy</span>
<span class="stat-value" id="accuracy">100%</span>
</div>
<div class="stat-item">
<span class="stat-label">Characters</span>
<span class="stat-value" id="charCount">0/0</span>
</div>
</div>
</div>
<div id="errorInfo" style="margin-top:16px;min-height:28px;font-weight:600;text-align:center;"></div>
<div class="progress-bar">
<div class="progress-fill" id="progress" style="width:0%"></div>
</div>
</div>
<div class="card">
<h2>🔥 Live Heatmap & Analysis</h2>
<div style="display:flex;gap:16px;flex-wrap:wrap;">
<div style="flex:1;min-width:280px;">
<div class="keyboard" id="keyboard"></div>
<div style="margin-top:12px;font-size:0.85rem;color:#aaa;">
<strong>Legend:</strong> Red = high error rate | Glow = uncertainty (need more data)
</div>
</div>
<div style="flex:1;min-width:280px;">
<div class="explanation" id="explanation">
<div style="color:#aaa;">Keep typing to see your personalized analysis...</div>
</div>
</div>
</div>
</div>
<div class="card">
<h2>🎮 Adaptive Drill Generator</h2>
<div style="margin-bottom:12px;color:#aaa;">
Generates practice text weighted by: <strong>error rate × (uncertainty + 0.1)</strong>
</div>
<div class="drill" id="drillText">Type at least 20 characters to generate your first personalized drill...</div>
<div style="margin-top:12px;display:flex;gap:8px;">
<button id="regenDrill">🔄 New Drill</button>
<button id="copyDrill">📋 Copy to Target</button>
</div>
</div>
<div class="card">
<h2>📈 Detailed Statistics</h2>
<div style="margin-bottom:12px;font-size:0.9rem;color:#aaa;">
Home row keys shown. High error + low uncertainty = consistent problem to focus on.
</div>
<div class="stats-grid" id="statsGrid"></div>
</div>
<div class="footer">
<div>Powered by Beta-Binomial Bayesian inference with temporal decay (λ=0.995)</div>
<div style="margin-top:4px;">All processing happens locally in your browser • No data leaves your device</div>
</div>
</div>
<script>
// Beta tracker with decay for temporal weighting
class BetaTracker {
constructor(alpha=1, beta=1, decay=0.995){
this.alpha = alpha;
this.beta = beta;
this.decay = decay;
this.counts = {};
this.totalObservations = 0;
this.recentErrors = [];
}
_ensure(k){
if(!this.counts[k]) {
this.counts[k] = {e: this.alpha, s: this.beta};
}
}
decayAll(){
Object.values(this.counts).forEach(c => {
c.e *= this.decay;
c.s *= this.decay;
});
}
observe(k, error){
this.decayAll();
this._ensure(k);
if(error) {
this.counts[k].e += 1;
this.recentErrors.push(k);
if(this.recentErrors.length > 10) this.recentErrors.shift();
} else {
this.counts[k].s += 1;
}
this.totalObservations++;
}
posterior(k){
this._ensure(k);
const {e, s} = this.counts[k];
const mean = e / (e + s);
const variance = (e * s) / ((e + s) * (e + s) * (e + s + 1));
const effectiveN = e + s - this.alpha - this.beta;
return {
mean,
sd: Math.sqrt(variance),
count: effectiveN,
alpha: e,
beta: s
};
}
getAllPosteriors() {
const posteriors = {};
'abcdefghijklmnopqrstuvwxyz'.split('').forEach(c => {
posteriors[c] = this.posterior(c);
});
return posteriors;
}
}
// Common words organized by difficulty and letter patterns
const WORD_CORPUS = {
// High-frequency words (top 100 most common)
common: [
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'would', 'her',
'was', 'one', 'our', 'out', 'day', 'had', 'has', 'his', 'how', 'man',
'its', 'say', 'she', 'which', 'their', 'time', 'will', 'way', 'about',
'many', 'then', 'them', 'write', 'like', 'these', 'long', 'make', 'thing',
'see', 'him', 'two', 'look', 'more', 'go', 'come', 'number', 'sound',
'most', 'people', 'over', 'know', 'water', 'than', 'call', 'first'
],
// Words with common digraphs
digraphs: {
'th': ['the', 'that', 'this', 'they', 'there', 'think', 'through', 'three', 'thanks', 'thought'],
'ch': ['change', 'check', 'choice', 'choose', 'chair', 'chance', 'charge', 'cheap', 'church', 'chapter'],
'sh': ['should', 'show', 'share', 'short', 'shape', 'sharp', 'shift', 'shine', 'shock', 'shoot'],
'wh': ['what', 'when', 'where', 'which', 'while', 'white', 'whole', 'whose', 'wheel', 'whether'],
'qu': ['quick', 'quite', 'quiet', 'queen', 'question', 'quality', 'quarter', 'square', 'require', 'equal'],
'ing': ['thing', 'being', 'doing', 'going', 'making', 'taking', 'coming', 'looking', 'working', 'thinking'],
'er': ['other', 'after', 'never', 'every', 'under', 'number', 'perhaps', 'better', 'together', 'remember'],
'ed': ['called', 'looked', 'asked', 'needed', 'wanted', 'worked', 'lived', 'turned', 'started', 'seemed']
},
// Words by difficulty (based on hand movements)
patterns: {
homeRow: ['had', 'ask', 'dad', 'sad', 'lad', 'fad', 'gas', 'has', 'lag', 'sag'],
topRow: ['were', 'your', 'trip', 'quit', 'power', 'write', 'quiet', 'worry', 'pretty', 'twenty'],
bottomRow: ['can', 'man', 'been', 'came', 'name', 'mean', 'become', 'common', 'woman', 'human'],
mixed: ['their', 'would', 'about', 'there', 'think', 'which', 'people', 'could', 'other', 'after']
},
// Common programming/tech words
technical: [
'function', 'variable', 'return', 'class', 'import', 'export', 'const', 'async', 'array', 'object',
'string', 'number', 'boolean', 'interface', 'public', 'private', 'static', 'method', 'property'
]
};
// Sentence templates for more natural practice
const SENTENCE_TEMPLATES = [
"The {adjective} {noun} {verb} {preposition} the {noun}.",
"{pronoun} {verb} to {verb} the {adjective} {noun}.",
"Can you {verb} the {noun} {preposition} the {adjective} {noun}?",
"{number} {adjective} {noun}s {verb} {adverb} {preposition} the {noun}.",
"The {noun} {verb} {adjective} and {adjective}."
];
const WORD_TYPES = {
adjective: ['quick', 'brown', 'lazy', 'beautiful', 'small', 'large', 'happy', 'sad', 'fast', 'slow'],
noun: ['fox', 'dog', 'cat', 'house', 'tree', 'book', 'computer', 'phone', 'desk', 'chair'],
verb: ['jumps', 'runs', 'walks', 'sits', 'stands', 'writes', 'reads', 'types', 'thinks', 'works'],
pronoun: ['I', 'you', 'he', 'she', 'we', 'they', 'it'],
preposition: ['over', 'under', 'beside', 'through', 'across', 'behind', 'near', 'between'],
adverb: ['quickly', 'slowly', 'carefully', 'happily', 'quietly', 'loudly'],
number: ['two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten']
};
// Enhanced drill generator
function generateDrill(posteriors, length = 50) {
if (tracker.totalObservations < 20) {
// Start with common words for beginners
return "Type these common words: the quick brown fox jumps over the lazy dog. Practice makes perfect!";
}
// Calculate focus scores for each letter
const letterScores = Object.entries(posteriors)
.filter(([k, v]) => v.count > 0)
.map(([k, v]) => ({
key: k,
focus: v.mean * (v.sd + 0.1),
errorRate: v.mean,
uncertainty: v.sd
}))
.sort((a, b) => b.focus - a.focus);
// Get top problematic letters
const problemLetters = letterScores.slice(0, 5).map(s => s.key);
// Categorize problem areas
const problemDigraphs = [];
const problemPatterns = [];
// Check for problematic digraphs
for (const [digraph, words] of Object.entries(WORD_CORPUS.digraphs)) {
if (digraph.split('').some(letter => problemLetters.includes(letter))) {
problemDigraphs.push({ pattern: digraph, words });
}
}
// Build adaptive drill
let drill = [];
let currentLength = 0;
// Mix different types of practice
while (currentLength < length) {
const random = Math.random();
if (random < 0.4 && problemDigraphs.length > 0) {
// 40% - Focus on problematic digraphs
const digraph = problemDigraphs[Math.floor(Math.random() * problemDigraphs.length)];
const word = digraph.words[Math.floor(Math.random() * digraph.words.length)];
drill.push(word);
currentLength += word.length + 1;
} else if (random < 0.7) {
// 30% - Words containing problem letters
const targetLetter = problemLetters[Math.floor(Math.random() * Math.min(3, problemLetters.length))];
const candidates = [
...WORD_CORPUS.common,
...Object.values(WORD_CORPUS.patterns).flat()
].filter(w => w.includes(targetLetter));
if (candidates.length > 0) {
const word = candidates[Math.floor(Math.random() * candidates.length)];
drill.push(word);
currentLength += word.length + 1;
}
} else if (random < 0.85) {
// 15% - Common words for flow
const word = WORD_CORPUS.common[Math.floor(Math.random() * WORD_CORPUS.common.length)];
drill.push(word);
currentLength += word.length + 1;
} else {
// 15% - Generate a short sentence
if (currentLength + 20 < length) {
const sentence = generateSentence(problemLetters);
drill.push(sentence);
currentLength += sentence.length + 1;
}
}
}
// Format the drill nicely
const drillText = drill.join(' ').trim();
// Add a note about what we're focusing on
const focusNote = problemLetters.length > 0
? `Focus areas: ${problemLetters.map(l => l.toUpperCase()).join(', ')} | `
: '';
return focusNote + drillText;
}
// Generate sentences with problem letters
function generateSentence(problemLetters) {
const template = SENTENCE_TEMPLATES[Math.floor(Math.random() * SENTENCE_TEMPLATES.length)];
let sentence = template;
// Replace placeholders with words containing problem letters when possible
for (const [type, words] of Object.entries(WORD_TYPES)) {
if (sentence.includes(`{${type}}`)) {
const candidates = words.filter(w =>
problemLetters.some(letter => w.includes(letter))
);
const wordList = candidates.length > 0 ? candidates : words;
const word = wordList[Math.floor(Math.random() * wordList.length)];
sentence = sentence.replace(`{${type}}`, word);
}
}
return sentence;
}
// Update the initial target text
function getInitialText() {
const introTexts = [
"Welcome to adaptive typing practice. Start with this sentence to build your profile.",
"Type this paragraph to help me understand your typing patterns and create personalized drills.",
"Every keystroke teaches me about your typing style. Let's begin with this warm-up text.",
"Practice makes perfect. Begin typing to discover your unique strengths and challenges.",
"Your personalized typing journey starts here. Type this text to establish your baseline."
];
return introTexts[Math.floor(Math.random() * introTexts.length)];
}
// Keyboard layout
const KEY_ROWS = [
['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']
];
const tracker = new BetaTracker(1, 1, 0.995);
let lastTypedLength = 0;
// DOM elements
const targetInput = document.getElementById('target');
const targetDisplay = document.getElementById('targetDisplay');
const typedArea = document.getElementById('typed');
const typingOverlay = document.getElementById('typingOverlay');
const keyboardDiv = document.getElementById('keyboard');
const explanationDiv = document.getElementById('explanation');
const drillDiv = document.getElementById('drillText');
const regenBtn = document.getElementById('regenDrill');
const copyBtn = document.getElementById('copyDrill');
const editTargetBtn = document.getElementById('editTargetBtn');
const statsGrid = document.getElementById('statsGrid');
const errorInfo = document.getElementById('errorInfo');
const resetBtn = document.getElementById('resetBtn');
const progressBar = document.getElementById('progress');
const wpmDisplay = document.getElementById('wpm');
const accuracyDisplay = document.getElementById('accuracy');
const charCountDisplay = document.getElementById('charCount');
// Typing statistics
let startTime = null;
let correctChars = 0;
let totalChars = 0;
// Save/Load functionality
function saveState() {
localStorage.setItem('bayesianTypingState', JSON.stringify({
counts: tracker.counts,
totalObservations: tracker.totalObservations
}));
}
function loadState() {
const saved = localStorage.getItem('bayesianTypingState');
if (saved) {
const state = JSON.parse(saved);
tracker.counts = state.counts || {};
tracker.totalObservations = state.totalObservations || 0;
}
}
// Initialize keyboard DOM with tooltips
function buildKeyboard(){
keyboardDiv.innerHTML = '';
KEY_ROWS.forEach(row => {
const r = document.createElement('div');
r.className = 'key-row';
row.forEach(k => {
const keyEl = document.createElement('div');
keyEl.className = 'key';
keyEl.dataset.key = k;
keyEl.innerHTML = `
<div class="key-letter">${k.toUpperCase()}</div>
<div class="key-stats">0.0%</div>
<div class="tooltip">No data yet</div>
`;
r.appendChild(keyEl);
});
keyboardDiv.appendChild(r);
});
}
function updateHeatmap(posteriors){
// Find max values for normalization
let maxMean = 0, maxSD = 0;
Object.values(posteriors).forEach(p => {
maxMean = Math.max(maxMean, p.mean);
maxSD = Math.max(maxSD, p.sd);
});
KEY_ROWS.forEach(row => {
row.forEach(k => {
const p = posteriors[k];
const keyEl = keyboardDiv.querySelector(`[data-key='${k}']`);
if(!p || !keyEl) return;
const mean = p.mean;
const sd = p.sd;
// Color based on error rate
const red = Math.min(255, Math.round(mean * 400));
const green = Math.max(0, 100 - Math.round(mean * 200));
// Glow based on uncertainty
const glowIntensity = Math.min(1, sd * 5);
const glowSize = Math.max(4, sd * 100);
keyEl.style.background = `linear-gradient(135deg,
rgba(${red}, ${green}, 50, ${Math.min(0.8, mean * 2)}) 0%,
rgba(255, 255, 255, ${Math.min(0.1, sd * 0.5)}) 100%)`;
if (sd > 0.02) {
keyEl.style.boxShadow = `0 0 ${glowSize}px rgba(255, 255, 255, ${glowIntensity * 0.4})`;
} else {
keyEl.style.boxShadow = 'none';
}
// Update stats display
const statsEl = keyEl.querySelector('.key-stats');
if(statsEl) {
statsEl.innerHTML = `${(mean * 100).toFixed(1)}%`;
}
// Update tooltip
const tooltip = keyEl.querySelector('.tooltip');
if(tooltip) {
tooltip.innerHTML = `
Error: ${(mean * 100).toFixed(1)}% ± ${(sd * 100).toFixed(1)}%<br>
Observations: ${Math.round(p.count)}
`;
}
});
});
}
// Enhanced drill generator
function generateDrill(posteriors, length = 80) {
if (tracker.totalObservations < 20) {
// Start with common words for beginners
return "Type these common words: the quick brown fox jumps over the lazy dog. Practice makes perfect!";
}
// Calculate focus scores for each letter
const letterScores = Object.entries(posteriors)
.filter(([k, v]) => v.count > 0)
.map(([k, v]) => ({
key: k,
focus: v.mean * (v.sd + 0.1),
errorRate: v.mean,
uncertainty: v.sd
}))
.sort((a, b) => b.focus - a.focus);
// Get top problematic letters
const problemLetters = letterScores.slice(0, 5).map(s => s.key);
// Categorize problem areas
const problemDigraphs = [];
// Check for problematic digraphs
for (const [digraph, words] of Object.entries(WORD_CORPUS.digraphs)) {
if (digraph.split('').some(letter => problemLetters.includes(letter))) {
problemDigraphs.push({ pattern: digraph, words });
}
}
// Build adaptive drill
let drill = [];
let currentLength = 0;
// Mix different types of practice
while (currentLength < length) {
const random = Math.random();
if (random < 0.4 && problemDigraphs.length > 0) {
// 40% - Focus on problematic digraphs
const digraph = problemDigraphs[Math.floor(Math.random() * problemDigraphs.length)];
const word = digraph.words[Math.floor(Math.random() * digraph.words.length)];
drill.push(word);
currentLength += word.length + 1;
} else if (random < 0.7) {
// 30% - Words containing problem letters
const targetLetter = problemLetters[Math.floor(Math.random() * Math.min(3, problemLetters.length))];
const candidates = [
...WORD_CORPUS.common,
...Object.values(WORD_CORPUS.patterns).flat()
].filter(w => w.includes(targetLetter));
if (candidates.length > 0) {
const word = candidates[Math.floor(Math.random() * candidates.length)];
drill.push(word);
currentLength += word.length + 1;
}
} else if (random < 0.85) {
// 15% - Common words for flow
const word = WORD_CORPUS.common[Math.floor(Math.random() * WORD_CORPUS.common.length)];
drill.push(word);
currentLength += word.length + 1;
} else {
// 15% - Generate a short sentence
if (currentLength + 20 < length) {
const sentence = generateSentence(problemLetters);
drill.push(sentence);
currentLength += sentence.length + 1;
}
}
}
// Format the drill nicely
const drillText = drill.join(' ').trim();
// Add a note about what we're focusing on
const focusNote = problemLetters.length > 0
? `Focus: ${problemLetters.slice(0,3).map(l => l.toUpperCase()).join(', ')} | `
: '';
return focusNote + drillText;
}
function updateExplanation(posteriors){
const arr = Object.entries(posteriors)
.filter(([k, v]) => v.count > 0)
.map(([k, v]) => ({k, ...v}));
arr.sort((a, b) => b.mean - a.mean);
if (tracker.totalObservations < 10) {
explanationDiv.innerHTML = `
<div style="color:#aaa;">
Keep typing! I need at least 10 keystrokes to start analyzing your patterns.
<div style="margin-top:8px;">Progress: ${tracker.totalObservations}/10</div>
</div>
`;
return;
}
const top = arr.slice(0, 3);
explanationDiv.innerHTML = '';
const title = document.createElement('div');
title.innerHTML = '<strong>Your Personal Typing Analysis:</strong>';
title.style.marginBottom = '12px';
explanationDiv.appendChild(title);
top.forEach((t, i) => {
const block = document.createElement('div');
block.style.marginTop = '8px';
let recommendation = "";
if (t.sd > 0.1) {
recommendation = "Need more data for confidence.";
} else if (t.mean > 0.15) {
recommendation = "High priority for practice!";
} else if (t.mean > 0.05) {
recommendation = "Room for improvement.";
} else {
recommendation = "Good accuracy!";
}
block.innerHTML = `
${i + 1}. <span class="highlight">${t.k.toUpperCase()}</span>
- ${(t.mean * 100).toFixed(1)}% errors
${(t.sd * 100).toFixed(1)}% uncertainty)
<div style="font-size:0.85rem;color:#aaa;margin-top:4px;">${recommendation}</div>
`;
explanationDiv.appendChild(block);
});
if (tracker.recentErrors.length > 0) {
const recentDiv = document.createElement('div');
recentDiv.style.marginTop = '16px';
recentDiv.innerHTML = `
<div style="font-size:0.9rem;color:#ef4444;">
Recent mistakes: ${tracker.recentErrors.slice(-5).join(', ')}
</div>
`;
explanationDiv.appendChild(recentDiv);
}
}
function updateStats(posteriors){
statsGrid.innerHTML = '';
const homeRowKeys = ['a','s','d','f','j','k','l',';'];
homeRowKeys.forEach(c => {
if (c === ';') return; // Skip semicolon for now
const p = posteriors[c];
const box = document.createElement('div');
box.className = 'stat-box';
// Color code based on performance
let performanceClass = '';
if (p.mean < 0.05) {
performanceClass = 'style="border-color: #4ade80;"';
} else if (p.mean > 0.15) {
performanceClass = 'style="border-color: #ef4444;"';
}
box.innerHTML = `
<div class="stat-key">${c.toUpperCase()}</div>
<div style="font-size:0.85rem;">
<div>Error rate: ${(p.mean * 100).toFixed(2)}%</div>
<div>Uncertainty: ±${(p.sd * 100).toFixed(2)}%</div>
<div style="color:#aaa;">Observations: ${Math.round(p.count)}</div>
</div>
`;
box.setAttribute('style', performanceClass);
statsGrid.appendChild(box);
});
}
function refreshAll(){
const post = tracker.getAllPosteriors();
updateHeatmap(post);
updateExplanation(post);
const drillText = generateDrill(post, 80);
drillDiv.textContent = drillText;
updateStats(post);
saveState();
}
// Update target display with character-by-character rendering
function updateTargetDisplay() {
const target = targetInput.value;
const typed = typedArea.value;
let html = '';
for (let i = 0; i < target.length; i++) {
const char = target[i];
let className = 'char';
if (i < typed.length) {
if (typed[i] === char) {
className += ' typed';
} else {
className += ' error';
}
} else if (i === typed.length) {
className += ' current';
}
html += `<span class="${className}">${char}</span>`;
}
targetDisplay.innerHTML = html;
}
// Main typing handler
typedArea.addEventListener('input', e => {
const typed = e.target.value;
const target = targetInput.value;
// Start timer on first character
if (!startTime && typed.length > 0) {
startTime = Date.now();
}
// Update visual display
updateTargetDisplay();
// Update progress bar
const progress = Math.min(100, (typed.length / target.length) * 100);
progressBar.style.width = progress + '%';
// Update character count
charCountDisplay.textContent = `${typed.length}/${target.length}`;
if(typed.length === 0) {
errorInfo.textContent = '';
errorInfo.className = '';
lastTypedLength = 0;
startTime = null;
correctChars = 0;
totalChars = 0;
wpmDisplay.textContent = '0';
accuracyDisplay.textContent = '100%';
refreshAll();
return;
}
// Only process new characters
if (typed.length > lastTypedLength) {
for (let i = lastTypedLength; i < typed.length; i++) {
const intended = (target[i] || '').toLowerCase();
const actual = typed[i].toLowerCase();
totalChars++;
if(intended.match(/[a-z]/)) {
const error = actual !== intended;
tracker.observe(intended, error);
if(error) {
errorInfo.innerHTML = `❌ Mistake: typed '<strong>${actual}</strong>' instead of '<strong>${intended}</strong>'`;
errorInfo.className = 'error-flash';
errorInfo.style.color = '#ef4444';
// Flash the key
const keyEl = keyboardDiv.querySelector(`[data-key='${intended}']`);
if (keyEl) {
keyEl.classList.add('error-flash');
setTimeout(() => keyEl.classList.remove('error-flash'), 500);
}
} else {
correctChars++;
if (i === typed.length - 1) {
errorInfo.innerHTML = `✓ Correct!`;
errorInfo.className = 'success-flash';
errorInfo.style.color = '#4ade80';
}
}
} else if (intended === actual) {
correctChars++;
}
}
}
lastTypedLength = typed.length;
// Update WPM
if (startTime && typed.length > 0) {
const minutes = (Date.now() - startTime) / 60000;
const words = typed.length / 5; // Standard: 5 chars = 1 word
const wpm = Math.round(words / minutes);
wpmDisplay.textContent = wpm;
}
// Update accuracy
if (totalChars > 0) {
const accuracy = (correctChars / totalChars * 100).toFixed(1);
accuracyDisplay.textContent = accuracy + '%';
}
// Check if completed
if (typed === target && typed.length > 0) {
const accuracy = (correctChars / totalChars * 100).toFixed(1);
setTimeout(() => {
errorInfo.innerHTML = `🎉 Perfect! Completed with ${accuracy}% accuracy at ${wpmDisplay.textContent} WPM!`;
errorInfo.style.color = '#4ade80';
}, 100);
}
refreshAll();
});
// Handle backspace to track corrections
typedArea.addEventListener('keydown', e => {
if (e.key === 'Backspace') {
lastTypedLength = Math.max(0, typedArea.value.length - 1);
}
});
// Button handlers
regenBtn.addEventListener('click', () => {
refreshAll();
const flashMsg = document.createElement('div');
flashMsg.style = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--primary);color:white;padding:16px 24px;border-radius:8px;z-index:1000;';
flashMsg.textContent = '✨ New drill generated!';
document.body.appendChild(flashMsg);
setTimeout(() => flashMsg.remove(), 1500);
});
copyBtn.addEventListener('click', () => {
targetInput.value = drillDiv.textContent;
typedArea.value = '';
typedArea.focus();
lastTypedLength = 0;
startTime = null;
correctChars = 0;
totalChars = 0;
progressBar.style.width = '0%';
updateTargetDisplay();
const flashMsg = document.createElement('div');
flashMsg.style = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--success);color:white;padding:16px 24px;border-radius:8px;z-index:1000;';
flashMsg.textContent = '📋 Drill copied to target!';
document.body.appendChild(flashMsg);
setTimeout(() => flashMsg.remove(), 1500);
});
editTargetBtn.addEventListener('click', () => {
targetInput.style.display = targetInput.style.display === 'none' ? 'inline-block' : 'none';
if (targetInput.style.display === 'none') {
editTargetBtn.textContent = '✏️ Edit';
} else {
editTargetBtn.textContent = '✓ Done';
targetInput.focus();
}
});
resetBtn.addEventListener('click', () => {
if(confirm('This will reset all your typing statistics. Are you sure?')) {
localStorage.removeItem('bayesianTypingState');
location.reload();
}
});
// Allow custom target text
targetInput.addEventListener('input', () => {
typedArea.value = '';
lastTypedLength = 0;
startTime = null;
correctChars = 0;
totalChars = 0;
progressBar.style.width = '0%';
errorInfo.textContent = '';
updateTargetDisplay();
});
// Initialize
loadState();
buildKeyboard();
refreshAll();
targetInput.style.display = 'none';
updateTargetDisplay();
// Focus on typing area
typedArea.focus();
</script>
</body>
</html>