mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-10 20:12:48 +00:00
feat: sentence trainer idle timeout
This commit is contained in:
@@ -13,21 +13,22 @@
|
||||
import TrackText from "$lib/charrecorder/TrackText.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
function initialThresholds(): [slow: number, fast: number][] {
|
||||
function viaLocalStorage<T>(key: string, initial: T) {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("mastery-thresholds") ?? "");
|
||||
return JSON.parse(localStorage.getItem(key) ?? "");
|
||||
} catch {
|
||||
return [
|
||||
[1500, 1050],
|
||||
[3000, 2500],
|
||||
[5000, 3500],
|
||||
[6000, 5000],
|
||||
];
|
||||
return initial;
|
||||
}
|
||||
}
|
||||
|
||||
let masteryThresholds: [slow: number, fast: number][] =
|
||||
$state(initialThresholds());
|
||||
let masteryThresholds: [slow: number, fast: number][] = $state(
|
||||
viaLocalStorage("mastery-thresholds", [
|
||||
[1500, 1050],
|
||||
[3000, 2500],
|
||||
[5000, 3500],
|
||||
[6000, 5000],
|
||||
]),
|
||||
);
|
||||
|
||||
let inputSentence = $derived(
|
||||
(browser && $page.url.searchParams.get("sentence")) || "Hello World",
|
||||
@@ -46,6 +47,10 @@
|
||||
let wpm = $state(0);
|
||||
let chords: InferredChord[] = $state([]);
|
||||
let recorder = $state(new ReplayRecorder());
|
||||
let idle = $state(true);
|
||||
let idleTime = $state(viaLocalStorage("idle-timeout", 100));
|
||||
|
||||
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
let cooldown = $state(false);
|
||||
|
||||
@@ -59,6 +64,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
localStorage.setItem("idle-timeout", idleTime.toString());
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
localStorage.setItem(
|
||||
"mastery-thresholds",
|
||||
@@ -148,7 +157,7 @@
|
||||
function checkInput() {
|
||||
if (recorder.player.stepper.challenge.length === 0) return;
|
||||
const replay = recorder.finish(false);
|
||||
const elapsed = replay.finish - replay.start!;
|
||||
const elapsed = replay.finish - replay.start! - idleTime;
|
||||
if (elapsed < masteryThresholds[level]![0]) {
|
||||
lastWPM = wpm;
|
||||
|
||||
@@ -157,19 +166,22 @@
|
||||
wordStats.set(currentWord, prevStats.slice(-10));
|
||||
}
|
||||
|
||||
cooldown = true;
|
||||
setTimeout(() => {
|
||||
selectNextWord();
|
||||
cooldown = false;
|
||||
});
|
||||
selectNextWord();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!cooldown && text && text === currentWord) checkInput();
|
||||
if (idle && text && text.trim() === currentWord.trim()) checkInput();
|
||||
});
|
||||
|
||||
function onkey(event: KeyboardEvent) {
|
||||
if (idleTimeout) {
|
||||
clearTimeout(idleTimeout);
|
||||
}
|
||||
idle = false;
|
||||
recorder.next(event);
|
||||
idleTimeout = setTimeout(() => {
|
||||
idle = true;
|
||||
}, idleTime);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -273,6 +285,7 @@
|
||||
</div>
|
||||
{#if devTools}
|
||||
<div>Dev Tools</div>
|
||||
<label>Idle Time <input bind:value={idleTime} /></label>
|
||||
<table>
|
||||
<tbody>
|
||||
{#each masteryThresholds as _, i}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
wordStats,
|
||||
level,
|
||||
masteryThresholds,
|
||||
}: { wordStats: object; level: number; masteryThresholds: number[] } =
|
||||
$props();
|
||||
|
||||
// Function to calculate average time for a word
|
||||
function calculateAverageTime(times: number[]): number {
|
||||
if (times.length === 0) return Number.NaN;
|
||||
const totalTime = times.reduce((a, b) => a + b, 0);
|
||||
return totalTime / times.length;
|
||||
}
|
||||
|
||||
// Function to calculate WPM for each timing
|
||||
function calculateWPM(elapsedTime: number, wordLength: number): number {
|
||||
const minutesElapsed = elapsedTime / 60000;
|
||||
return Math.floor(wordLength / minutesElapsed);
|
||||
}
|
||||
|
||||
// Function to get the best WPM for a word
|
||||
function getBestWPM(times: number[], wordLength: number): number {
|
||||
if (times.length === 0) return Number.NaN;
|
||||
const wpms = times.map((time) => calculateWPM(time, wordLength));
|
||||
return Math.max(...wpms);
|
||||
}
|
||||
|
||||
function getLastWPM(times: number[], wordLength: number) {
|
||||
const lastTime = times[times.length - 1];
|
||||
return lastTime === undefined
|
||||
? Number.NaN
|
||||
: calculateWPM(lastTime, wordLength);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="stats-table">
|
||||
<h2>Stats for Level {level}</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Word</th>
|
||||
<th>Last WPM</th>
|
||||
<!-- Individualized -->
|
||||
<th>Best WPM</th>
|
||||
<!-- Individualized -->
|
||||
<th>Attempts</th>
|
||||
<th>Average Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(wordStats) as [word, times]}
|
||||
<tr>
|
||||
<td>{word}</td>
|
||||
<td>{getLastWPM(times, word.split(" ").length)}</td>
|
||||
<!-- Individualized -->
|
||||
<td>{getBestWPM(times, word.split(" ").length)}</td>
|
||||
<!-- Individualized -->
|
||||
<td>{times.length}</td>
|
||||
<td>{calculateAverageTime(times)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stats-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f4f4f4;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user