diff --git a/src/components/rnfgrertt/calculateStats.ts b/src/components/rnfgrertt/calculateStats.ts index 8af718e..eb8e9d9 100644 --- a/src/components/rnfgrertt/calculateStats.ts +++ b/src/components/rnfgrertt/calculateStats.ts @@ -21,14 +21,17 @@ export const calculateStats = ( } } + let errorsMade = errors.length; + let correctKeystrokes = userInput.length - errorsMade; + // Calculate WPM (only correct words) - const wpm = (correctChars / 5) * (60 / timeElapsed); + const wpm = (correctKeystrokes / 5) * (60 / timeElapsed); // Calculate Raw WPM (including incorrect words) const rawWpm = (userInput.length / 5) * (60 / timeElapsed); // Calculate accuracy - const accuracy = (correctChars / totalKeystrokes) * 100; + const accuracy = (correctKeystrokes / totalKeystrokes) * 100; // Calculate character ratio const incorrectChars = totalKeystrokes - correctChars; @@ -38,12 +41,24 @@ export const calculateStats = ( // Calculate consistency using coefficient of variation of raw WPM const rawWpmValues = wpmData.map((point) => point.wpm); const mean = rawWpmValues.reduce((a, b) => a + b, 0) / rawWpmValues.length; - const variance = - rawWpmValues.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / - rawWpmValues.length; - const stdDev = Math.sqrt(variance); - const cv = (stdDev / mean) * 100; - const consistency = Math.max(0, Math.min(100, 100 - cv)); + + // Calculate weighted standard deviation + const weightedVariance = + rawWpmValues.reduce((acc, wpm) => { + const diff = wpm - mean; + // Apply smaller weight to variations at higher speeds + const weight = Math.max(0.5, 100 / mean); + return acc + diff * diff * weight; + }, 0) / rawWpmValues.length; + + const weightedStdDev = Math.sqrt(weightedVariance); + + // Adjust consistency calculation for higher WPM + const baseConsistency = Math.max(0, 100 - (weightedStdDev / mean) * 100); + const consistency = Math.min( + 100, + baseConsistency * (1 + Math.log10(mean / 100)), + ); return { wpm: wpm, diff --git a/src/components/rnfgrertt/helpModal.tsx b/src/components/rnfgrertt/helpModal.tsx new file mode 100644 index 0000000..6dde289 --- /dev/null +++ b/src/components/rnfgrertt/helpModal.tsx @@ -0,0 +1,44 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ArrowUp, ChevronUp, Command } from "lucide-react"; + +export const HelpModal = ({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) => { + return ( + + + + type@tools help + +
+
+ reset test + + + esc + +
+
+ rotate cursor style + - +
+
+ help menu (this page) + + /{" "} + + h + +
+
+
+
+ ); +}; diff --git a/src/components/rnfgrertt/hooks/useWpmTracker.tsx b/src/components/rnfgrertt/hooks/useWpmTracker.tsx index 0a0b694..9c83a08 100644 --- a/src/components/rnfgrertt/hooks/useWpmTracker.tsx +++ b/src/components/rnfgrertt/hooks/useWpmTracker.tsx @@ -15,7 +15,7 @@ export const useWpmTracker = ( useEffect(() => { userInputRef.current = userInput; - if (userInputRef.current.length >= 5 && wpmData.length < 1) { + if (userInputRef.current.length >= 10 && wpmData.length < 1) { updateWPMData(); } }, [userInput]); @@ -51,7 +51,7 @@ export const useWpmTracker = ( calculateMetrics(now); // Calculate errors per second - const timeDiff = (now - (lastUpdateRef.current || now)) / 250; + const timeDiff = (now - (lastUpdateRef.current || now)) / UPDATE_INTERVAL; const errorDelta = currentErrors - prevErrorsRef.current; const errorsPerSecond = timeDiff > 0 ? errorDelta / timeDiff : 0; diff --git a/src/components/rnfgrertt/resultsView.tsx b/src/components/rnfgrertt/resultsView.tsx index f69d12e..5784017 100644 --- a/src/components/rnfgrertt/resultsView.tsx +++ b/src/components/rnfgrertt/resultsView.tsx @@ -60,7 +60,7 @@ export const ResultsView = ({
-
+
@@ -144,7 +144,7 @@ const StatBox = ({
{label}
-
+
{typeof value === "number" ? value.toFixed(0) : value} {following}
@@ -203,7 +203,7 @@ const PerformanceChart = ({ wpmData }: { wpmData: WPMDataPoint[] }) => ( dataKey="time" type="number" label={{ value: "Time (seconds)", position: "bottom" }} - domain={[0, "max"]} + domain={[1, "max"]} tickCount={Math.max(Math.ceil(wpmData.length / 8), 10)} /> {

time: {data.time}s

wpm: {data.wpm}

-

raw: {data.wpm}

+

raw: {data.rawWpm}

errors: {data.errorsPerSecond}

); diff --git a/src/components/rnfgrertt/textGenerator.ts b/src/components/rnfgrertt/textGenerator.ts index 73d6a82..95d23bd 100644 --- a/src/components/rnfgrertt/textGenerator.ts +++ b/src/components/rnfgrertt/textGenerator.ts @@ -11,23 +11,27 @@ export const generateWords = (count: number): string => { ); }; -export const getRandomText = (length: "short" | "med" | "long") => { +export const getRandomText = (length: "short" | "med" | "long" | "xl") => { let minLen = 0; let maxLen = 0; switch (length) { case "short": - minLen = 50; - maxLen = 100; + minLen = 0; + maxLen = 75; break; case "med": - minLen = 100; + minLen = 75; maxLen = 200; break; case "long": minLen = 200; maxLen = 400; break; + case "xl": + minLen = 400; + maxLen = 999; + break; } let quoteSelection = engQuotes.quotes.filter( diff --git a/src/components/rnfgrertt/types.ts b/src/components/rnfgrertt/types.ts index d921800..b9f8286 100644 --- a/src/components/rnfgrertt/types.ts +++ b/src/components/rnfgrertt/types.ts @@ -29,3 +29,5 @@ export type TextMeta = { }; export type TimerOption = 15 | 30 | 60 | 120; + +export type CursorStyle = "block" | "line" | "underline"; diff --git a/src/components/rnfgrertt/typingArea.tsx b/src/components/rnfgrertt/typingArea.tsx index 9eae5ca..1bbad6e 100644 --- a/src/components/rnfgrertt/typingArea.tsx +++ b/src/components/rnfgrertt/typingArea.tsx @@ -1,7 +1,8 @@ -import { useRef, useEffect } from "preact/hooks"; +import { useRef, useEffect, useState } from "preact/hooks"; import { TIMER_OPTIONS } from "./constants"; -import { TimerOption } from "./types"; -import { ComponentChild, VNode } from "preact"; +import { CursorStyle, TimerOption } from "./types"; +import { KbdKey } from "../ui/kbdKey"; +import { isOnMac } from "../smartSearchBar"; export const TypingArea = ({ userInput, @@ -16,6 +17,7 @@ export const TypingArea = ({ onSelectQuoteLen, randomTextLen, onSelectRandomTextLen, + cursorStyle = "block", }: { userInput: string; handleInput: (e: React.ChangeEvent) => void; @@ -25,66 +27,78 @@ export const TypingArea = ({ selectedTime: TimerOption | null; onSelectTime: (time: TimerOption) => void; timeRemaining: number | null; - selectedQuoteLen: "short" | "med" | "long"; - onSelectQuoteLen: (len: "short" | "med" | "long") => void; + selectedQuoteLen: "short" | "med" | "long" | "xl"; + onSelectQuoteLen: (len: "short" | "med" | "long" | "xl") => void; randomTextLen: TimerOption | null; onSelectRandomTextLen: (time: TimerOption) => void; + cursorStyle: CursorStyle; }) => { const textareaRef = useRef(null); useEffect(() => { textareaRef.current?.focus(); - }, []); + }, [sampleText]); return ( -
-
-
- - onSelectMode(option as "random" | "quotes" | "time") - } - /> - {selectedMode === "time" ? ( - onSelectTime(option as TimerOption)} - suffix="s" - /> - ) : selectedMode === "quotes" ? ( - - onSelectQuoteLen(option as "short" | "med" | "long") - } - /> - ) : ( +
+
+
+
- onSelectRandomTextLen(option as TimerOption) + onSelectMode(option as "random" | "quotes" | "time") } /> - )} +
+ {selectedMode === "time" ? ( + onSelectTime(option as TimerOption)} + suffix="s" + /> + ) : selectedMode === "quotes" ? ( + + onSelectQuoteLen(option as "short" | "med" | "long" | "xl") + } + /> + ) : ( + + onSelectRandomTextLen(option as TimerOption) + } + /> + )} +
+
{timeRemaining?.toFixed(0)}
+
+
+ +