Skip to content

Typing accuracy, UX improvements, and bug fixes#1

Open
hilaryduffrules-hash wants to merge 5 commits intomurdawkmedia:masterfrom
hilaryduffrules-hash:fix/improvements
Open

Typing accuracy, UX improvements, and bug fixes#1
hilaryduffrules-hash wants to merge 5 commits intomurdawkmedia:masterfrom
hilaryduffrules-hash:fix/improvements

Conversation

@hilaryduffrules-hash
Copy link

Summary

This PR fixes several real bugs that affect core functionality, adds keyboard typing mode alongside voice, and improves accessibility and mobile handling — all while keeping the privacy-first, local-only architecture intact.


🐛 Bug Fixes

Critical: Speech transcript concatenated without spaces (App.tsx)

The original code joined all speech recognition segments with ''.join(''):

// Before — 'hello' + 'world' = 'helloworld'
.join('')

// After — trims each segment, joins with space
.map((r) => r[0].transcript.trim()).filter(Boolean).join(' ')

This caused the word-matching logic to almost never fire correctly when the recognizer split the transcript across multiple segments (which happens on any pause in speech).

Critical: Stale closure on latestRef (App.tsx)

latestRef was updated inside a useEffect, which runs asynchronously after paint. A speech recognition result arriving in that window would call the old handleSpeechInput with stale quote state — causing it to match against the previous quote.

Fixed by updating latestRef.current directly in the render function body (synchronous, no effect needed). React refs are safe to write during render.

TypeScript error: onInputChange required but never passed (TypingArea.tsx)

onInputChange was a required prop in the interface but App.tsx never passed it. Added ? to make it optional. Added companion onKeystroke? for per-keystroke accuracy.

cursor-blink animation never fired (TypingArea.tsx + index.html)

The CSS declared @keyframes blink but the class was cursor-blink. Name mismatch meant the cursor never blinked. Fixed by renaming to @keyframes cursor-blink and changing border-color to inherit so it works with both dark and light theme border colours (was hardcoded to #0071e3).

WPM could produce NaN or Infinity (App.tsx)

The WPM useEffect ran immediately when status changed to RUNNING, before any time had elapsed, producing 0 / 0 = NaN. Added a timeElapsedMin <= 0 guard.

recognition.stop() could throw on reset (App.tsx)

resetTest called recognitionRef.current.stop() without a try/catch. If recognition had already ended (e.g. silence timeout), this would throw an uncaught exception. Wrapped in try/catch.


✨ New Features

Keyboard typing mode (⌨️ Keys)

Added a Voice / Keys toggle in the nav. Keyboard mode:

  • Renders a hidden <input> inside TypingArea that captures keystrokes on all platforms including mobile (with autoComplete off, autoCorrect off, autoCapitalize off, enterKeyHint='next').
  • Auto-focuses the input when the test starts so users don't need to click anything.
  • Clicking anywhere in the text area re-focuses the input if it loses focus.
  • Tracks per-keystroke accuracy via onKeyDown (before the value changes), so backspace-corrected errors are still counted.
  • Advances to the next quote automatically when the full text is matched exactly.

Real accuracy tracking in keyboard mode

Accuracy = (correct keystrokes / total keystrokes) × 100, updated live. Voice mode continues to show 100% which is correct by design (only exact word matches advance the cursor).

Richer results card

Final results now show: Speed (WPM), Accuracy, Words typed, Characters typed, and Error count (keyboard) / mode icon (voice).

WPM color coding in StatsOverlay

  • 🔴 < 30 WPM
  • 🟡 30–59 WPM
  • 🟢 ≥ 60 WPM

Formatted time display

Durations ≥ 60 s now show as M:SS (e.g. 1:02) instead of a bare second count.


♿ Accessibility

  • aria-pressed on mode and duration toggle buttons.
  • role='status' + aria-live='polite' on the stats overlay.
  • Individual aria-label on each stat value.
  • role='region' + aria-label on the typing area.
  • aria-label on dark/light toggle, start button, and listening indicator.
  • SVG icons marked aria-hidden='true'.

📱 Mobile

  • touch-action: manipulation on all buttons — eliminates 300 ms tap delay and prevents accidental double-tap zoom.
  • -webkit-text-size-adjust: 100% prevents iOS from inflating font sizes on landscape rotation.
  • Hidden keyboard input uses mobile-friendly attributes (autoCapitalize, enterKeyHint, etc.).
  • Nav wraps on small screens via flex-wrap.

🏷️ Branding / Meta

  • Rename title from SwiftVoiceSwiftType (consistent with repo name and project intent).
  • Update OG, Twitter, and JSON-LD descriptions to mention both modes and privacy-first local processing.
  • Footer updated: replaces generic tagline with 'Privacy-first. No data leaves your browser.'

Privacy

No changes to the privacy model. All speech recognition uses the browser-native Web Speech API. No data is sent to any external service. The keyboard path is fully local.

Add InputMode enum (VOICE | KEYBOARD) to support the new dual-input
mode. Add wordsTyped to TypingStats for richer results display.
…, a11y

- Make onInputChange optional — it was required in the interface but never
  passed from App.tsx, causing a TypeScript error in strict mode.
- Add onKeystroke prop for per-keystroke accuracy tracking in keyboard mode.
- Add hidden <input> for keyboard capture with proper mobile attributes
  (autoComplete/Correct/Capitalize off, enterKeyHint, touch-action).
- Apply cursor-blink class correctly using the CSS animation name.
- Block Tab in onKeyDown so global reset handler can intercept it.
- Add role, aria-label, and aria-describedby for screen reader support.
- enableKeyboard prop auto-focuses the hidden input when test starts.
…, a11y

- Format time as M:SS (e.g. '1:02') for durations >= 60 s so the 120 s
  test is readable at a glance instead of showing a bare second count.
- Color-code WPM: red < 30, amber 30-59, green >= 60 for instant feedback.
- Accept totalDuration prop needed to decide the time format.
- Add role='status', aria-live='polite', and individual aria-label attrs
  so screen readers announce stat changes.
…le closure

Bug fixes:
- CRITICAL: Speech transcript was joined with ''.join('') — segments like
  'hello' and 'world' were concatenated as 'helloworld' instead of
  'hello world'. Fixed: trim each segment and join with ' '.
- CRITICAL: latestRef was updated via useEffect (async) — a speech result
  arriving before the effect ran would see stale state. Fixed by updating
  latestRef.current synchronously in the render body.
- WPM calculation: used sessionStats from outer closure which could be
  stale. Fixed: moved update inside setSessionStats updater to always
  read fresh prev state.
- recognition.stop() in finishTest was not wrapped in try/catch — could
  throw if recognition was already stopped.
- TestStatus.STARTING was declared but never used; startTest now has a
  clean path without dead states.

New features:
- Keyboard mode (⌨️ Keys): full keyboard typing test alongside voice,
  switchable via nav toggle. Resets the test on switch.
- Real per-keystroke accuracy in keyboard mode: tracked via onKeystroke
  callback from TypingArea's onKeyDown (before the input value changes),
  giving accurate error counts including backspace-corrected mistakes.
- Words-typed counter in final results.
- Improved final results card: Speed, Accuracy, Words, Chars, Errors/Mode.
- Voice accuracy correctly shows 100% (voice only advances on exact match).

UX improvements:
- Duration buttons and mode toggle both use aria-pressed.
- Start button has aria-label describing the active mode.
- 'Listening…' indicator has role='status' and aria-label.
- Footer updated to emphasise privacy-first, local-only processing.
- Nav is responsive with flex-wrap for small screens.
- Restart button label simplified to 'Restart'.
- Tab-key hint uses <kbd> element for semantics.
…blink

- Rename title from 'SwiftVoice' to 'SwiftType' (consistent with repo name).
- Update OG/Twitter/JSON-LD titles and descriptions to mention both voice
  and keyboard modes, and emphasise privacy-first local processing.
- Fix cursor-blink @Keyframes name to match the CSS class name (was
  'blink', class was 'cursor-blink' — animation never fired).
- Change @Keyframes to use 'inherit' for border-color so it works with
  both dark (blue-400) and light (blue-500) themes, instead of hardcoded
  #0071e3 which only matched the light theme.
- Add 'touch-action: manipulation' on buttons to prevent 300ms tap delay
  and accidental double-tap zoom on mobile.
- Add '-webkit-text-size-adjust: 100%' to prevent iOS font size inflation
  on landscape rotation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant