ariG23498 HF Staff commited on
Commit
f7e8d8a
·
verified ·
1 Parent(s): f414293

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1076 -19
index.html CHANGED
@@ -1,19 +1,1076 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Bayesian Touch Typing Tutor - Learn Smarter, Type Faster</title>
7
+ <style>
8
+ :root {
9
+ --bg:#0f111a;
10
+ --card:#1f2235;
11
+ --radius:16px;
12
+ --primary:#7c5aff;
13
+ --success:#4ade80;
14
+ --error:#ef4444;
15
+ font-family: system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
16
+ }
17
+ *{box-sizing:border-box;}
18
+ body{margin:0; background:linear-gradient(135deg,#0f111a,#1f2235); color:#e8ebf7; min-height:100vh;}
19
+
20
+ /* Header styles */
21
+ .header {
22
+ background: rgba(31, 34, 53, 0.8);
23
+ backdrop-filter: blur(10px);
24
+ padding: 24px;
25
+ border-bottom: 1px solid rgba(255,255,255,0.1);
26
+ position: sticky;
27
+ top: 0;
28
+ z-index: 100;
29
+ }
30
+
31
+ .hero-section {
32
+ text-align: center;
33
+ padding: 40px 20px;
34
+ background: linear-gradient(135deg, rgba(124, 90, 255, 0.1), rgba(74, 222, 128, 0.1));
35
+ margin-bottom: 24px;
36
+ }
37
+
38
+ .hero-title {
39
+ font-size: 3rem;
40
+ margin: 0 0 16px 0;
41
+ background: linear-gradient(135deg, #7c5aff, #4ade80);
42
+ -webkit-background-clip: text;
43
+ -webkit-text-fill-color: transparent;
44
+ }
45
+
46
+ .hero-subtitle {
47
+ font-size: 1.2rem;
48
+ color: #aaa;
49
+ margin-bottom: 32px;
50
+ }
51
+
52
+ .benefit-cards {
53
+ display: grid;
54
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
55
+ gap: 20px;
56
+ max-width: 900px;
57
+ margin: 0 auto 40px;
58
+ }
59
+
60
+ .benefit-card {
61
+ background: rgba(31, 34, 53, 0.6);
62
+ padding: 24px;
63
+ border-radius: 12px;
64
+ border: 1px solid rgba(255,255,255,0.1);
65
+ transition: all 0.3s;
66
+ }
67
+
68
+ .benefit-card:hover {
69
+ transform: translateY(-4px);
70
+ border-color: var(--primary);
71
+ box-shadow: 0 8px 24px rgba(124, 90, 255, 0.2);
72
+ }
73
+
74
+ .benefit-icon {
75
+ font-size: 2rem;
76
+ margin-bottom: 12px;
77
+ }
78
+
79
+ .benefit-title {
80
+ font-size: 1.1rem;
81
+ font-weight: 600;
82
+ margin-bottom: 8px;
83
+ color: var(--primary);
84
+ }
85
+
86
+ .benefit-desc {
87
+ font-size: 0.9rem;
88
+ color: #aaa;
89
+ line-height: 1.5;
90
+ }
91
+
92
+ .container{max-width:1100px;margin:0 auto;padding:24px;display:grid;gap:24px;grid-template-columns:1fr 1fr;}
93
+ .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);}
94
+ h2{margin-top:0;font-size:1.3rem;display:flex;align-items:center;gap:8px;}
95
+
96
+ textarea{
97
+ width:100%;
98
+ background:#0f132d;
99
+ border:2px solid #2a2f55;
100
+ padding:16px;
101
+ border-radius:8px;
102
+ color:#fff;
103
+ font-family:'Fira Code', 'Courier New', monospace;
104
+ font-size:1.2rem;
105
+ line-height:1.8;
106
+ resize:none;
107
+ transition: all 0.3s;
108
+ }
109
+
110
+ input{
111
+ width:100%;
112
+ background:#0f132d;
113
+ border:2px solid #2a2f55;
114
+ padding:12px;
115
+ border-radius:8px;
116
+ color:#fff;
117
+ font-family:'Fira Code', 'Courier New', monospace;
118
+ resize:none;
119
+ transition: all 0.3s;
120
+ }
121
+
122
+ input:focus, textarea:focus {
123
+ outline: none;
124
+ border-color: var(--primary);
125
+ box-shadow: 0 0 0 3px rgba(124, 90, 255, 0.2);
126
+ }
127
+
128
+ .typing-container {
129
+ background: #0f132d;
130
+ border-radius: 12px;
131
+ padding: 24px;
132
+ margin-top: 16px;
133
+ }
134
+
135
+ .target-section {
136
+ margin-bottom: 24px;
137
+ }
138
+
139
+ .target-display {
140
+ background: #1f2235;
141
+ padding: 20px;
142
+ border-radius: 8px;
143
+ font-family: 'Fira Code', 'Courier New', monospace;
144
+ font-size: 1.3rem;
145
+ line-height: 1.8;
146
+ color: #e8ebf7;
147
+ min-height: 80px;
148
+ border: 2px solid #2a2f55;
149
+ position: relative;
150
+ white-space: pre-wrap;
151
+ word-wrap: break-word;
152
+ }
153
+
154
+ .target-display .char {
155
+ position: relative;
156
+ transition: all 0.2s;
157
+ }
158
+
159
+ .target-display .typed {
160
+ color: #4ade80;
161
+ background: rgba(74, 222, 128, 0.1);
162
+ }
163
+
164
+ .target-display .error {
165
+ color: #ef4444;
166
+ background: rgba(239, 68, 68, 0.2);
167
+ animation: shake 0.3s;
168
+ }
169
+
170
+ .target-display .current {
171
+ background: rgba(124, 90, 255, 0.3);
172
+ box-shadow: 0 0 0 2px var(--primary);
173
+ animation: pulse 1s infinite;
174
+ }
175
+
176
+ @keyframes pulse {
177
+ 0%, 100% { opacity: 1; }
178
+ 50% { opacity: 0.6; }
179
+ }
180
+
181
+ @keyframes shake {
182
+ 0%, 100% { transform: translateX(0); }
183
+ 25% { transform: translateX(-2px); }
184
+ 75% { transform: translateX(2px); }
185
+ }
186
+
187
+ .typing-section {
188
+ position: relative;
189
+ }
190
+
191
+ .typing-overlay {
192
+ position: absolute;
193
+ top: 40px;
194
+ left: 0;
195
+ right: 0;
196
+ pointer-events: none;
197
+ padding: 16px;
198
+ font-family: 'Fira Code', 'Courier New', monospace;
199
+ font-size: 1.2rem;
200
+ line-height: 1.8;
201
+ color: transparent;
202
+ white-space: pre-wrap;
203
+ word-wrap: break-word;
204
+ }
205
+
206
+ .typing-stats {
207
+ display: flex;
208
+ gap: 24px;
209
+ margin-top: 20px;
210
+ padding-top: 20px;
211
+ border-top: 1px solid #2a2f55;
212
+ }
213
+
214
+ .stat-item {
215
+ flex: 1;
216
+ text-align: center;
217
+ }
218
+
219
+ .stat-label {
220
+ display: block;
221
+ font-size: 0.85rem;
222
+ color: #888;
223
+ margin-bottom: 4px;
224
+ }
225
+
226
+ .stat-value {
227
+ display: block;
228
+ font-size: 1.8rem;
229
+ font-weight: 700;
230
+ color: var(--primary);
231
+ }
232
+
233
+ button{
234
+ background:var(--primary);
235
+ border:none;
236
+ color:#fff;
237
+ padding:12px 20px;
238
+ border-radius:8px;
239
+ cursor:pointer;
240
+ font-weight:600;
241
+ transition: all 0.3s;
242
+ display: inline-flex;
243
+ align-items: center;
244
+ gap: 8px;
245
+ }
246
+
247
+ button:hover {
248
+ transform: translateY(-2px);
249
+ box-shadow: 0 4px 12px rgba(124, 90, 255, 0.4);
250
+ }
251
+
252
+ .keyboard{display:flex;flex-direction:column;gap:6px;margin-top:16px;}
253
+ .key-row{display:flex;justify-content:center;gap:6px;}
254
+ .key{
255
+ position:relative;
256
+ width:52px;
257
+ height:52px;
258
+ border-radius:8px;
259
+ display:flex;
260
+ flex-direction:column;
261
+ align-items:center;
262
+ justify-content:center;
263
+ font-weight:700;
264
+ user-select:none;
265
+ border:2px solid #2a2f55;
266
+ transition: all 0.3s;
267
+ cursor: default;
268
+ }
269
+
270
+ .key:hover {
271
+ transform: scale(1.05);
272
+ }
273
+
274
+ .key-letter {
275
+ font-size: 1.2rem;
276
+ }
277
+
278
+ .key-stats {
279
+ font-size: 0.65rem;
280
+ color: #aaa;
281
+ margin-top: 2px;
282
+ }
283
+
284
+ .drill{
285
+ background:#0f132d;
286
+ padding:16px;
287
+ border-radius:8px;
288
+ font-family:'Fira Code', monospace;
289
+ font-size:1.1rem;
290
+ line-height:1.6;
291
+ border: 2px solid #2a2f55;
292
+ }
293
+
294
+ .explanation{
295
+ background:#0f132d;
296
+ padding:16px;
297
+ border-radius:8px;
298
+ font-size:0.9rem;
299
+ line-height:1.6;
300
+ }
301
+
302
+ .stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;}
303
+ .stat-box{
304
+ background:#0f132d;
305
+ padding:16px;
306
+ border-radius:8px;
307
+ border:1px solid #2a2f55;
308
+ transition: all 0.3s;
309
+ }
310
+
311
+ .stat-box:hover {
312
+ border-color: var(--primary);
313
+ }
314
+
315
+ .stat-key {
316
+ font-size: 1.4rem;
317
+ font-weight: 700;
318
+ color: var(--primary);
319
+ margin-bottom: 8px;
320
+ }
321
+
322
+ .footer{grid-column:1/-1;text-align:center;font-size:0.8rem;margin-top:40px;color:#999;}
323
+
324
+ .highlight{
325
+ background:rgba(124, 90, 255, 0.2);
326
+ padding:4px 8px;
327
+ border-radius:6px;
328
+ font-weight:600;
329
+ color: var(--primary);
330
+ }
331
+
332
+ .error-flash {
333
+ animation: errorFlash 0.5s;
334
+ }
335
+
336
+ @keyframes errorFlash {
337
+ 0% { background-color: rgba(239, 68, 68, 0.3); }
338
+ 100% { background-color: transparent; }
339
+ }
340
+
341
+ .success-flash {
342
+ animation: successFlash 0.5s;
343
+ }
344
+
345
+ @keyframes successFlash {
346
+ 0% { background-color: rgba(74, 222, 128, 0.3); }
347
+ 100% { background-color: transparent; }
348
+ }
349
+
350
+ .progress-bar {
351
+ height: 4px;
352
+ background: #2a2f55;
353
+ border-radius: 2px;
354
+ overflow: hidden;
355
+ margin-top: 8px;
356
+ }
357
+
358
+ .progress-fill {
359
+ height: 100%;
360
+ background: linear-gradient(90deg, var(--primary), var(--success));
361
+ transition: width 0.3s;
362
+ }
363
+
364
+ .info-box {
365
+ background: rgba(124, 90, 255, 0.1);
366
+ border: 1px solid rgba(124, 90, 255, 0.3);
367
+ padding: 12px;
368
+ border-radius: 8px;
369
+ margin-bottom: 16px;
370
+ font-size: 0.9rem;
371
+ }
372
+
373
+ .tooltip {
374
+ position: absolute;
375
+ bottom: 100%;
376
+ left: 50%;
377
+ transform: translateX(-50%);
378
+ background: #2a2f55;
379
+ padding: 8px 12px;
380
+ border-radius: 6px;
381
+ font-size: 0.8rem;
382
+ white-space: nowrap;
383
+ opacity: 0;
384
+ pointer-events: none;
385
+ transition: opacity 0.3s;
386
+ margin-bottom: 8px;
387
+ z-index: 1000;
388
+ }
389
+
390
+ .key:hover .tooltip {
391
+ opacity: 1;
392
+ }
393
+
394
+ @media (max-width: 768px) {
395
+ .container { grid-template-columns: 1fr; }
396
+ .hero-title { font-size: 2rem; }
397
+ .key { width: 42px; height: 42px; }
398
+ }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div class="header">
403
+ <div style="max-width:1100px;margin:0 auto;display:flex;gap:16px;flex-wrap:wrap;align-items:center;">
404
+ <div style="flex:1;min-width:280px;">
405
+ <h1 style="margin:0;font-size:1.5rem;">🧠 Bayesian Touch Typing Tutor</h1>
406
+ </div>
407
+ <div style="flex:1;min-width:280px;text-align:right;">
408
+ <button id="resetBtn">🔄 Reset All Stats</button>
409
+ </div>
410
+ </div>
411
+ </div>
412
+
413
+ <div class="hero-section">
414
+ <h1 class="hero-title">Learn to Type Smarter, Not Harder</h1>
415
+ <p class="hero-subtitle">Using advanced Bayesian statistics to create personalized typing exercises based on YOUR unique patterns</p>
416
+
417
+ <div class="benefit-cards">
418
+ <div class="benefit-card">
419
+ <div class="benefit-icon">🎯</div>
420
+ <div class="benefit-title">Adaptive Learning</div>
421
+ <div class="benefit-desc">The tutor learns which keys you struggle with and creates custom drills targeting your weak spots</div>
422
+ </div>
423
+ <div class="benefit-card">
424
+ <div class="benefit-icon">📊</div>
425
+ <div class="benefit-title">Uncertainty-Aware</div>
426
+ <div class="benefit-desc">Knows when it needs more data about a key before making strong recommendations</div>
427
+ </div>
428
+ <div class="benefit-card">
429
+ <div class="benefit-icon">⚡</div>
430
+ <div class="benefit-title">Real-Time Feedback</div>
431
+ <div class="benefit-desc">Visual heatmap shows your error patterns instantly, with glow indicating confidence levels</div>
432
+ </div>
433
+ </div>
434
+ </div>
435
+
436
+ <div class="container">
437
+ <div class="card" style="grid-column: 1 / -1;">
438
+ <h2>📝 Typing Area</h2>
439
+ <div class="info-box">
440
+ 💡 <strong>How it works:</strong> Type the target text below. Each keystroke updates your personal error model using Bayesian inference.
441
+ </div>
442
+
443
+ <div class="typing-container">
444
+ <div class="target-section">
445
+ <label style="font-weight:600;color:#7c5aff;margin-bottom:8px;display:block;">Target Text:</label>
446
+ <div class="target-display" id="targetDisplay"></div>
447
+ <div style="margin-top:12px;">
448
+ <input id="target" value="the quick brown fox jumps over the lazy dog" placeholder="Enter custom text to practice" style="font-size:0.9rem;">
449
+ <button id="editTargetBtn" style="margin-left:8px;padding:8px 16px;font-size:0.9rem;">✏️ Edit</button>
450
+ </div>
451
+ </div>
452
+
453
+ <div class="typing-section">
454
+ <label style="font-weight:600;color:#4ade80;margin-bottom:8px;display:block;">Your Typing:</label>
455
+ <div class="typing-overlay" id="typingOverlay"></div>
456
+ <textarea id="typed" rows="3" placeholder="Start typing the target text above..." spellcheck="false"></textarea>
457
+ </div>
458
+
459
+ <div class="typing-stats">
460
+ <div class="stat-item">
461
+ <span class="stat-label">WPM</span>
462
+ <span class="stat-value" id="wpm">0</span>
463
+ </div>
464
+ <div class="stat-item">
465
+ <span class="stat-label">Accuracy</span>
466
+ <span class="stat-value" id="accuracy">100%</span>
467
+ </div>
468
+ <div class="stat-item">
469
+ <span class="stat-label">Characters</span>
470
+ <span class="stat-value" id="charCount">0/0</span>
471
+ </div>
472
+ </div>
473
+ </div>
474
+
475
+ <div id="errorInfo" style="margin-top:16px;min-height:28px;font-weight:600;text-align:center;"></div>
476
+ <div class="progress-bar">
477
+ <div class="progress-fill" id="progress" style="width:0%"></div>
478
+ </div>
479
+ </div>
480
+
481
+ <div class="card">
482
+ <h2>🔥 Live Heatmap & Analysis</h2>
483
+ <div style="display:flex;gap:16px;flex-wrap:wrap;">
484
+ <div style="flex:1;min-width:280px;">
485
+ <div class="keyboard" id="keyboard"></div>
486
+ <div style="margin-top:12px;font-size:0.85rem;color:#aaa;">
487
+ <strong>Legend:</strong> Red = high error rate | Glow = uncertainty (need more data)
488
+ </div>
489
+ </div>
490
+ <div style="flex:1;min-width:280px;">
491
+ <div class="explanation" id="explanation">
492
+ <div style="color:#aaa;">Keep typing to see your personalized analysis...</div>
493
+ </div>
494
+ </div>
495
+ </div>
496
+ </div>
497
+
498
+ <div class="card">
499
+ <h2>🎮 Adaptive Drill Generator</h2>
500
+ <div style="margin-bottom:12px;color:#aaa;">
501
+ Generates practice text weighted by: <strong>error rate × (uncertainty + 0.1)</strong>
502
+ </div>
503
+ <div class="drill" id="drillText">Type at least 20 characters to generate your first personalized drill...</div>
504
+ <div style="margin-top:12px;display:flex;gap:8px;">
505
+ <button id="regenDrill">🔄 New Drill</button>
506
+ <button id="copyDrill">📋 Copy to Target</button>
507
+ </div>
508
+ </div>
509
+
510
+ <div class="card">
511
+ <h2>📈 Detailed Statistics</h2>
512
+ <div style="margin-bottom:12px;font-size:0.9rem;color:#aaa;">
513
+ Home row keys shown. High error + low uncertainty = consistent problem to focus on.
514
+ </div>
515
+ <div class="stats-grid" id="statsGrid"></div>
516
+ </div>
517
+
518
+ <div class="footer">
519
+ <div>Powered by Beta-Binomial Bayesian inference with temporal decay (λ=0.995)</div>
520
+ <div style="margin-top:4px;">All processing happens locally in your browser • No data leaves your device</div>
521
+ </div>
522
+ </div>
523
+
524
+ <script>
525
+ // Beta tracker with decay for temporal weighting
526
+ class BetaTracker {
527
+ constructor(alpha=1, beta=1, decay=0.995){
528
+ this.alpha = alpha;
529
+ this.beta = beta;
530
+ this.decay = decay;
531
+ this.counts = {};
532
+ this.totalObservations = 0;
533
+ this.recentErrors = [];
534
+ }
535
+
536
+ _ensure(k){
537
+ if(!this.counts[k]) {
538
+ this.counts[k] = {e: this.alpha, s: this.beta};
539
+ }
540
+ }
541
+
542
+ decayAll(){
543
+ Object.values(this.counts).forEach(c => {
544
+ c.e *= this.decay;
545
+ c.s *= this.decay;
546
+ });
547
+ }
548
+
549
+ observe(k, error){
550
+ this.decayAll();
551
+ this._ensure(k);
552
+ if(error) {
553
+ this.counts[k].e += 1;
554
+ this.recentErrors.push(k);
555
+ if(this.recentErrors.length > 10) this.recentErrors.shift();
556
+ } else {
557
+ this.counts[k].s += 1;
558
+ }
559
+ this.totalObservations++;
560
+ }
561
+
562
+ posterior(k){
563
+ this._ensure(k);
564
+ const {e, s} = this.counts[k];
565
+ const mean = e / (e + s);
566
+ const variance = (e * s) / ((e + s) * (e + s) * (e + s + 1));
567
+ const effectiveN = e + s - this.alpha - this.beta;
568
+ return {
569
+ mean,
570
+ sd: Math.sqrt(variance),
571
+ count: effectiveN,
572
+ alpha: e,
573
+ beta: s
574
+ };
575
+ }
576
+
577
+ getAllPosteriors() {
578
+ const posteriors = {};
579
+ 'abcdefghijklmnopqrstuvwxyz'.split('').forEach(c => {
580
+ posteriors[c] = this.posterior(c);
581
+ });
582
+ return posteriors;
583
+ }
584
+ }
585
+
586
+ // Common words for drill generation
587
+ const COMMON_WORDS = [
588
+ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had',
589
+ 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'how',
590
+ 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy',
591
+ 'did', 'car', 'let', 'put', 'say', 'she', 'too', 'use'
592
+ ];
593
+
594
+ // Keyboard layout
595
+ const KEY_ROWS = [
596
+ ['q','w','e','r','t','y','u','i','o','p'],
597
+ ['a','s','d','f','g','h','j','k','l'],
598
+ ['z','x','c','v','b','n','m']
599
+ ];
600
+
601
+ const tracker = new BetaTracker(1, 1, 0.995);
602
+ let lastTypedLength = 0;
603
+
604
+ // DOM elements
605
+ const targetInput = document.getElementById('target');
606
+ const targetDisplay = document.getElementById('targetDisplay');
607
+ const typedArea = document.getElementById('typed');
608
+ const typingOverlay = document.getElementById('typingOverlay');
609
+ const keyboardDiv = document.getElementById('keyboard');
610
+ const explanationDiv = document.getElementById('explanation');
611
+ const drillDiv = document.getElementById('drillText');
612
+ const regenBtn = document.getElementById('regenDrill');
613
+ const copyBtn = document.getElementById('copyDrill');
614
+ const editTargetBtn = document.getElementById('editTargetBtn');
615
+ const statsGrid = document.getElementById('statsGrid');
616
+ const errorInfo = document.getElementById('errorInfo');
617
+ const resetBtn = document.getElementById('resetBtn');
618
+ const progressBar = document.getElementById('progress');
619
+ const wpmDisplay = document.getElementById('wpm');
620
+ const accuracyDisplay = document.getElementById('accuracy');
621
+ const charCountDisplay = document.getElementById('charCount');
622
+
623
+ // Typing statistics
624
+ let startTime = null;
625
+ let correctChars = 0;
626
+ let totalChars = 0;
627
+
628
+ // Save/Load functionality
629
+ function saveState() {
630
+ localStorage.setItem('bayesianTypingState', JSON.stringify({
631
+ counts: tracker.counts,
632
+ totalObservations: tracker.totalObservations
633
+ }));
634
+ }
635
+
636
+ function loadState() {
637
+ const saved = localStorage.getItem('bayesianTypingState');
638
+ if (saved) {
639
+ const state = JSON.parse(saved);
640
+ tracker.counts = state.counts || {};
641
+ tracker.totalObservations = state.totalObservations || 0;
642
+ }
643
+ }
644
+
645
+ // Initialize keyboard DOM with tooltips
646
+ function buildKeyboard(){
647
+ keyboardDiv.innerHTML = '';
648
+ KEY_ROWS.forEach(row => {
649
+ const r = document.createElement('div');
650
+ r.className = 'key-row';
651
+ row.forEach(k => {
652
+ const keyEl = document.createElement('div');
653
+ keyEl.className = 'key';
654
+ keyEl.dataset.key = k;
655
+ keyEl.innerHTML = `
656
+ <div class="key-letter">${k.toUpperCase()}</div>
657
+ <div class="key-stats">0.0%</div>
658
+ <div class="tooltip">No data yet</div>
659
+ `;
660
+ r.appendChild(keyEl);
661
+ });
662
+ keyboardDiv.appendChild(r);
663
+ });
664
+ }
665
+
666
+ function updateHeatmap(posteriors){
667
+ // Find max values for normalization
668
+ let maxMean = 0, maxSD = 0;
669
+ Object.values(posteriors).forEach(p => {
670
+ maxMean = Math.max(maxMean, p.mean);
671
+ maxSD = Math.max(maxSD, p.sd);
672
+ });
673
+
674
+ KEY_ROWS.forEach(row => {
675
+ row.forEach(k => {
676
+ const p = posteriors[k];
677
+ const keyEl = keyboardDiv.querySelector(`[data-key='${k}']`);
678
+ if(!p || !keyEl) return;
679
+
680
+ const mean = p.mean;
681
+ const sd = p.sd;
682
+
683
+ // Color based on error rate
684
+ const red = Math.min(255, Math.round(mean * 400));
685
+ const green = Math.max(0, 100 - Math.round(mean * 200));
686
+
687
+ // Glow based on uncertainty
688
+ const glowIntensity = Math.min(1, sd * 5);
689
+ const glowSize = Math.max(4, sd * 100);
690
+
691
+ keyEl.style.background = `linear-gradient(135deg,
692
+ rgba(${red}, ${green}, 50, ${Math.min(0.8, mean * 2)}) 0%,
693
+ rgba(255, 255, 255, ${Math.min(0.1, sd * 0.5)}) 100%)`;
694
+
695
+ if (sd > 0.02) {
696
+ keyEl.style.boxShadow = `0 0 ${glowSize}px rgba(255, 255, 255, ${glowIntensity * 0.4})`;
697
+ } else {
698
+ keyEl.style.boxShadow = 'none';
699
+ }
700
+
701
+ // Update stats display
702
+ const statsEl = keyEl.querySelector('.key-stats');
703
+ if(statsEl) {
704
+ statsEl.innerHTML = `${(mean * 100).toFixed(1)}%`;
705
+ }
706
+
707
+ // Update tooltip
708
+ const tooltip = keyEl.querySelector('.tooltip');
709
+ if(tooltip) {
710
+ tooltip.innerHTML = `
711
+ Error: ${(mean * 100).toFixed(1)}% ± ${(sd * 100).toFixed(1)}%<br>
712
+ Observations: ${Math.round(p.count)}
713
+ `;
714
+ }
715
+ });
716
+ });
717
+ }
718
+
719
+ function generateDrill(posteriors, length = 40){
720
+ if (tracker.totalObservations < 20) {
721
+ return "Type the target text above to build your personalized error model...";
722
+ }
723
+
724
+ // Calculate focus scores
725
+ const scores = Object.entries(posteriors)
726
+ .filter(([k, v]) => v.count > 0)
727
+ .map(([k, v]) => ({
728
+ key: k,
729
+ focus: v.mean * (v.sd + 0.1),
730
+ mean: v.mean,
731
+ sd: v.sd
732
+ }));
733
+
734
+ scores.sort((a, b) => b.focus - a.focus);
735
+
736
+ // Get top problematic keys
737
+ const topKeys = scores.slice(0, Math.min(5, scores.length));
738
+ if (topKeys.length === 0) return "Great job! Keep practicing to maintain your skills.";
739
+
740
+ // Generate drill text
741
+ let drill = "";
742
+ const targetLength = length;
743
+
744
+ while (drill.length < targetLength) {
745
+ // 70% chance to use a word containing problem keys
746
+ if (Math.random() < 0.7 && topKeys.length > 0) {
747
+ const targetKey = topKeys[Math.floor(Math.random() * topKeys.length)];
748
+ const wordsWithKey = COMMON_WORDS.filter(w => w.includes(targetKey.key));
749
+
750
+ if (wordsWithKey.length > 0) {
751
+ drill += wordsWithKey[Math.floor(Math.random() * wordsWithKey.length)] + " ";
752
+ } else {
753
+ // If no common word contains the key, create a simple pattern
754
+ const patterns = [
755
+ targetKey.key + targetKey.key,
756
+ targetKey.key + "a" + targetKey.key,
757
+ targetKey.key + "e" + targetKey.key,
758
+ "a" + targetKey.key + "a"
759
+ ];
760
+ drill += patterns[Math.floor(Math.random() * patterns.length)] + " ";
761
+ }
762
+ } else {
763
+ // Add a random common word
764
+ drill += COMMON_WORDS[Math.floor(Math.random() * COMMON_WORDS.length)] + " ";
765
+ }
766
+ }
767
+
768
+ return drill.trim();
769
+ }
770
+
771
+ function updateExplanation(posteriors){
772
+ const arr = Object.entries(posteriors)
773
+ .filter(([k, v]) => v.count > 0)
774
+ .map(([k, v]) => ({k, ...v}));
775
+
776
+ arr.sort((a, b) => b.mean - a.mean);
777
+
778
+ if (tracker.totalObservations < 10) {
779
+ explanationDiv.innerHTML = `
780
+ <div style="color:#aaa;">
781
+ Keep typing! I need at least 10 keystrokes to start analyzing your patterns.
782
+ <div style="margin-top:8px;">Progress: ${tracker.totalObservations}/10</div>
783
+ </div>
784
+ `;
785
+ return;
786
+ }
787
+
788
+ const top = arr.slice(0, 3);
789
+ explanationDiv.innerHTML = '';
790
+
791
+ const title = document.createElement('div');
792
+ title.innerHTML = '<strong>Your Personal Typing Analysis:</strong>';
793
+ title.style.marginBottom = '12px';
794
+ explanationDiv.appendChild(title);
795
+
796
+ top.forEach((t, i) => {
797
+ const block = document.createElement('div');
798
+ block.style.marginTop = '8px';
799
+
800
+ let recommendation = "";
801
+ if (t.sd > 0.1) {
802
+ recommendation = "Need more data for confidence.";
803
+ } else if (t.mean > 0.15) {
804
+ recommendation = "High priority for practice!";
805
+ } else if (t.mean > 0.05) {
806
+ recommendation = "Room for improvement.";
807
+ } else {
808
+ recommendation = "Good accuracy!";
809
+ }
810
+
811
+ block.innerHTML = `
812
+ ${i + 1}. <span class="highlight">${t.k.toUpperCase()}</span>
813
+ - ${(t.mean * 100).toFixed(1)}% errors
814
+ (±${(t.sd * 100).toFixed(1)}% uncertainty)
815
+ <div style="font-size:0.85rem;color:#aaa;margin-top:4px;">${recommendation}</div>
816
+ `;
817
+ explanationDiv.appendChild(block);
818
+ });
819
+
820
+ if (tracker.recentErrors.length > 0) {
821
+ const recentDiv = document.createElement('div');
822
+ recentDiv.style.marginTop = '16px';
823
+ recentDiv.innerHTML = `
824
+ <div style="font-size:0.9rem;color:#ef4444;">
825
+ Recent mistakes: ${tracker.recentErrors.slice(-5).join(', ')}
826
+ </div>
827
+ `;
828
+ explanationDiv.appendChild(recentDiv);
829
+ }
830
+ }
831
+
832
+ function updateStats(posteriors){
833
+ statsGrid.innerHTML = '';
834
+ const homeRowKeys = ['a','s','d','f','j','k','l',';'];
835
+
836
+ homeRowKeys.forEach(c => {
837
+ if (c === ';') return; // Skip semicolon for now
838
+
839
+ const p = posteriors[c];
840
+ const box = document.createElement('div');
841
+ box.className = 'stat-box';
842
+
843
+ // Color code based on performance
844
+ let performanceClass = '';
845
+ if (p.mean < 0.05) {
846
+ performanceClass = 'style="border-color: #4ade80;"';
847
+ } else if (p.mean > 0.15) {
848
+ performanceClass = 'style="border-color: #ef4444;"';
849
+ }
850
+
851
+ box.innerHTML = `
852
+ <div class="stat-key">${c.toUpperCase()}</div>
853
+ <div style="font-size:0.85rem;">
854
+ <div>Error rate: ${(p.mean * 100).toFixed(2)}%</div>
855
+ <div>Uncertainty: ±${(p.sd * 100).toFixed(2)}%</div>
856
+ <div style="color:#aaa;">Observations: ${Math.round(p.count)}</div>
857
+ </div>
858
+ `;
859
+
860
+ box.setAttribute('style', performanceClass);
861
+ statsGrid.appendChild(box);
862
+ });
863
+ }
864
+
865
+ function refreshAll(){
866
+ const post = tracker.getAllPosteriors();
867
+ updateHeatmap(post);
868
+ updateExplanation(post);
869
+
870
+ const drillText = generateDrill(post, 50);
871
+ drillDiv.textContent = drillText;
872
+
873
+ updateStats(post);
874
+ saveState();
875
+ }
876
+
877
+ // Update target display with character-by-character rendering
878
+ function updateTargetDisplay() {
879
+ const target = targetInput.value;
880
+ const typed = typedArea.value;
881
+
882
+ let html = '';
883
+ for (let i = 0; i < target.length; i++) {
884
+ const char = target[i];
885
+ let className = 'char';
886
+
887
+ if (i < typed.length) {
888
+ if (typed[i] === char) {
889
+ className += ' typed';
890
+ } else {
891
+ className += ' error';
892
+ }
893
+ } else if (i === typed.length) {
894
+ className += ' current';
895
+ }
896
+
897
+ html += `<span class="${className}">${char}</span>`;
898
+ }
899
+
900
+ targetDisplay.innerHTML = html;
901
+ }
902
+
903
+ // Main typing handler
904
+ typedArea.addEventListener('input', e => {
905
+ const typed = e.target.value;
906
+ const target = targetInput.value;
907
+
908
+ // Start timer on first character
909
+ if (!startTime && typed.length > 0) {
910
+ startTime = Date.now();
911
+ }
912
+
913
+ // Update visual display
914
+ updateTargetDisplay();
915
+
916
+ // Update progress bar
917
+ const progress = Math.min(100, (typed.length / target.length) * 100);
918
+ progressBar.style.width = progress + '%';
919
+
920
+ // Update character count
921
+ charCountDisplay.textContent = `${typed.length}/${target.length}`;
922
+
923
+ if(typed.length === 0) {
924
+ errorInfo.textContent = '';
925
+ errorInfo.className = '';
926
+ lastTypedLength = 0;
927
+ startTime = null;
928
+ correctChars = 0;
929
+ totalChars = 0;
930
+ wpmDisplay.textContent = '0';
931
+ accuracyDisplay.textContent = '100%';
932
+ refreshAll();
933
+ return;
934
+ }
935
+
936
+ // Only process new characters
937
+ if (typed.length > lastTypedLength) {
938
+ for (let i = lastTypedLength; i < typed.length; i++) {
939
+ const intended = (target[i] || '').toLowerCase();
940
+ const actual = typed[i].toLowerCase();
941
+
942
+ totalChars++;
943
+
944
+ if(intended.match(/[a-z]/)) {
945
+ const error = actual !== intended;
946
+ tracker.observe(intended, error);
947
+
948
+ if(error) {
949
+ errorInfo.innerHTML = `❌ Mistake: typed '<strong>${actual}</strong>' instead of '<strong>${intended}</strong>'`;
950
+ errorInfo.className = 'error-flash';
951
+ errorInfo.style.color = '#ef4444';
952
+
953
+ // Flash the key
954
+ const keyEl = keyboardDiv.querySelector(`[data-key='${intended}']`);
955
+ if (keyEl) {
956
+ keyEl.classList.add('error-flash');
957
+ setTimeout(() => keyEl.classList.remove('error-flash'), 500);
958
+ }
959
+ } else {
960
+ correctChars++;
961
+ if (i === typed.length - 1) {
962
+ errorInfo.innerHTML = `✓ Correct!`;
963
+ errorInfo.className = 'success-flash';
964
+ errorInfo.style.color = '#4ade80';
965
+ }
966
+ }
967
+ } else if (intended === actual) {
968
+ correctChars++;
969
+ }
970
+ }
971
+ }
972
+
973
+ lastTypedLength = typed.length;
974
+
975
+ // Update WPM
976
+ if (startTime && typed.length > 0) {
977
+ const minutes = (Date.now() - startTime) / 60000;
978
+ const words = typed.length / 5; // Standard: 5 chars = 1 word
979
+ const wpm = Math.round(words / minutes);
980
+ wpmDisplay.textContent = wpm;
981
+ }
982
+
983
+ // Update accuracy
984
+ if (totalChars > 0) {
985
+ const accuracy = (correctChars / totalChars * 100).toFixed(1);
986
+ accuracyDisplay.textContent = accuracy + '%';
987
+ }
988
+
989
+ // Check if completed
990
+ if (typed === target && typed.length > 0) {
991
+ const accuracy = (correctChars / totalChars * 100).toFixed(1);
992
+ setTimeout(() => {
993
+ errorInfo.innerHTML = `🎉 Perfect! Completed with ${accuracy}% accuracy at ${wpmDisplay.textContent} WPM!`;
994
+ errorInfo.style.color = '#4ade80';
995
+ }, 100);
996
+ }
997
+
998
+ refreshAll();
999
+ });
1000
+
1001
+ // Handle backspace to track corrections
1002
+ typedArea.addEventListener('keydown', e => {
1003
+ if (e.key === 'Backspace') {
1004
+ lastTypedLength = Math.max(0, typedArea.value.length - 1);
1005
+ }
1006
+ });
1007
+
1008
+ // Button handlers
1009
+ regenBtn.addEventListener('click', () => {
1010
+ refreshAll();
1011
+ const flashMsg = document.createElement('div');
1012
+ 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;';
1013
+ flashMsg.textContent = '✨ New drill generated!';
1014
+ document.body.appendChild(flashMsg);
1015
+ setTimeout(() => flashMsg.remove(), 1500);
1016
+ });
1017
+
1018
+ copyBtn.addEventListener('click', () => {
1019
+ targetInput.value = drillDiv.textContent;
1020
+ typedArea.value = '';
1021
+ typedArea.focus();
1022
+ lastTypedLength = 0;
1023
+ startTime = null;
1024
+ correctChars = 0;
1025
+ totalChars = 0;
1026
+ progressBar.style.width = '0%';
1027
+ updateTargetDisplay();
1028
+
1029
+ const flashMsg = document.createElement('div');
1030
+ 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;';
1031
+ flashMsg.textContent = '📋 Drill copied to target!';
1032
+ document.body.appendChild(flashMsg);
1033
+ setTimeout(() => flashMsg.remove(), 1500);
1034
+ });
1035
+
1036
+ editTargetBtn.addEventListener('click', () => {
1037
+ targetInput.style.display = targetInput.style.display === 'none' ? 'inline-block' : 'none';
1038
+ if (targetInput.style.display === 'none') {
1039
+ editTargetBtn.textContent = '✏️ Edit';
1040
+ } else {
1041
+ editTargetBtn.textContent = '✓ Done';
1042
+ targetInput.focus();
1043
+ }
1044
+ });
1045
+
1046
+ resetBtn.addEventListener('click', () => {
1047
+ if(confirm('This will reset all your typing statistics. Are you sure?')) {
1048
+ localStorage.removeItem('bayesianTypingState');
1049
+ location.reload();
1050
+ }
1051
+ });
1052
+
1053
+ // Allow custom target text
1054
+ targetInput.addEventListener('input', () => {
1055
+ typedArea.value = '';
1056
+ lastTypedLength = 0;
1057
+ startTime = null;
1058
+ correctChars = 0;
1059
+ totalChars = 0;
1060
+ progressBar.style.width = '0%';
1061
+ errorInfo.textContent = '';
1062
+ updateTargetDisplay();
1063
+ });
1064
+
1065
+ // Initialize
1066
+ loadState();
1067
+ buildKeyboard();
1068
+ refreshAll();
1069
+ targetInput.style.display = 'none';
1070
+ updateTargetDisplay();
1071
+
1072
+ // Focus on typing area
1073
+ typedArea.focus();
1074
+ </script>
1075
+ </body>
1076
+ </html>