Typing Mentor – Technical Design Specification

1. Purpose and scope

This document describes the architecture and implementation plan for a browser-based typing trainer integrated into an existing Astro site. It is written for senior-level engineers and is intended to be used by Codex CLI as a concise but complete technical reference.

The initial scope (Phase 1) is:

Later phases build on this foundation (additional lesson types, analytics, AI-assisted coaching), but are out of scope for this version of the spec except where they influence current design decisions.


2. Technology stack

Key design principle: strict separation of domain logic from UI. The “typing engine” must be a pure TypeScript module with no React or DOM dependencies.


3. High-level architecture

3.1. Page integration

3.2. Project structure (frontend part)

Under src/components/TypingMentor:

Under src/core/typing-mentor (domain logic, no React):

This separation should be strict: src/core/typing-mentor/* has no imports from React or component tree.


4. Domain model (core)

All types below live in src/core/typing-mentor/types.ts unless further split is justified.

4.1. Keyboard layout

Use KeyboardEvent.code as the primary identifier for keys (physical location), not event.key. This allows multiple logical layouts on the same physical keyboard.

export type LayoutId = "en-US" | "ru-RU"; // extensible, more layouts can be added later

export type KeyCode = string; // e.g. "KeyA", "Space", "Digit1", "ShiftLeft"

export interface KeyDefinition {
  code: KeyCode;            // corresponds to KeyboardEvent.code
  base: string;             // default printed symbol (e.g. "a", "ф", "1")
  shift?: string;           // symbol when Shift is held (e.g. "A", "Ф", "!")
  isModifier?: boolean;     // Shift, Ctrl, Alt, Meta, CapsLock, etc.
  width?: number;           // relative width (1 = base unit, 2, 2.25, etc.)
  align?: "left" | "center" | "right";
}

export interface KeyboardRow {
  keys: KeyDefinition[];
}

export interface KeyboardLayout {
  id: LayoutId;
  name: string;             // "English (US)", "Russian"
  rows: KeyboardRow[];
}

Concrete layouts live in layout.ts as constants, e.g. EN_US_LAYOUT, RU_RU_LAYOUT.

4.2. Lessons and exercises

Phase 1 will use a simple “fixed text” lesson, but the model is extensible for future lesson types.

export type LessonKind = "fixedText" | "generatedSequence" | "wordList" | "externalText";

export type LessonId = string;

export interface Lesson {
  id: LessonId;
  title: string;
  description?: string;
  layoutId: LayoutId;
  kind: LessonKind;
  difficulty: number; // 1..10, purely indicative
  config: LessonConfig;
}

export type LessonConfig =
  | FixedTextLessonConfig
  | GeneratedSequenceConfig
  | WordListConfig
  | ExternalTextConfig;

export interface FixedTextLessonConfig {
  kind: "fixedText";
  text: string;
}

export interface GeneratedSequenceConfig {
  kind: "generatedSequence";
  characters: string[];  // e.g. [ "ф", "ы", "в" ]
  length: number;        // length of generated text
}

export interface WordListConfig {
  kind: "wordList";
  words: string[];       // list of words for this lesson
  totalCharacters: number;
}

export interface ExternalTextConfig {
  kind: "externalText";
  sourceId: string;      // placeholder for future integration (user text, URL, etc.)
}

In Phase 1, only FixedTextLessonConfig needs to be implemented; other variants must be typed but can throw NotImplemented if used.

4.3. Typing session state

A session represents one run of a single lesson.

export interface Keystroke {
  index: number;          // index in text (0-based), or -1 for non-text keys
  expected: string | null;
  actual: string;
  time: number;           // timestamp (ms since epoch)
  isError: boolean;
}

export type SessionStatus = "idle" | "running" | "finished";

export interface SessionState {
  lessonId: LessonId;
  layoutId: LayoutId;

  text: string;           // resolved exercise text
  position: number;       // current caret index (0..text.length)
  errors: number;         // total error count (see behaviour below)
  startedAt: number | null;
  finishedAt: number | null;
  status: SessionStatus;

  keystrokes: Keystroke[];
}

4.4. Session events

Session is updated exclusively via events, consumed by a pure reducer function.

export type SessionEvent =
  | { type: "START" }
  | { type: "KEY_PRESS"; char: string; code: KeyCode }
  | { type: "BACKSPACE" }
  | { type: "FINISH" }
  | { type: "RESET"; lesson: Lesson; text?: string };

4.5. Derived statistics

Defined in stats.ts:

export interface SessionStats {
  durationMs: number;
  charactersTyped: number;
  grossSpeedCpm: number;      // characters per minute (raw, incl. errors)
  netSpeedCpm: number;        // characters per minute minus penalties
  wpm: number;                // words per minute (5 chars per word convention)
  accuracy: number;           // 0..1
}

calculateStats(state: SessionState): SessionStats:

4.6. Behaviour rules for Phase 1

Reducer signature (in session.ts):

export function createInitialSession(lesson: Lesson, resolvedText: string): SessionState { /* ... */ }

export function sessionReducer(state: SessionState, event: SessionEvent): SessionState { /* pure */ }

5. UI components (React)

5.1. TypingMentor

Location: src/components/TypingMentor/TypingMentor.tsx

Responsibilities:

Key points:

5.2. Keyboard

Location: src/components/TypingMentor/Keyboard.tsx

Props:

interface KeyboardProps {
  layout: KeyboardLayout;
  pressedKeys: Set<KeyCode>;
  isShiftPressed: boolean;
  isCapsLockOn: boolean;
  expectedChar?: string | null;    // optional: highlight key corresponding to current target char
  onKeyClick?: (code: KeyCode) => void;
}

Rendering:

5.3. Key

Location: src/components/TypingMentor/Key.tsx

Props:

interface KeyProps {
  definition: KeyDefinition;
  isPressed: boolean;
  isShiftPressed: boolean;
  isCapsLockOn: boolean;
  isTarget?: boolean;             // highlight expected key for current character
  onClick?: (code: KeyCode) => void;
}

Display logic:

Styling requirements:

5.4. TextExercise

Location: src/components/TypingMentor/TextExercise.tsx

Props:

interface TextExerciseProps {
  text: string;
  position: number;
  keystrokes: Keystroke[];
}

Rendering:

5.5. SessionStats

Location: src/components/TypingMentor/SessionStats.tsx

Props:

interface SessionStatsProps {
  stats: SessionStats;
  status: SessionStatus;
}

Rendering:

5.6. LessonSelector

Phase 1 can be minimal: one or a few hardcoded lessons.

Props:

interface LessonSelectorProps {
  lessons: Lesson[];
  selectedLessonId: LessonId;
  onSelectLesson: (id: LessonId) => void;
}

Rendering:


6. Keyboard handling details

6.1. Use KeyboardEvent.code

6.2. Focus management

6.3. Handling modifiers

6.4. Mouse interaction

Phase 1 requirement: at least visual press animation for mouse clicks; mapping to the session is optional but recommended.


7. File layout and naming conventions

Suggested layout:

Names are indicative; they can be slightly adjusted to fit existing project conventions, but the separation between /core and /components must be preserved.


8. Styling guidelines

No inline styles except where necessary for proportional key width; prefer CSS classes and custom properties when possible.


9. Testing and quality

9.1. Unit tests (core)

Even if the project does not yet use a test runner, code should be written in a testable way (pure functions, no hidden globals).

9.2. Manual QA checklist

9.3. Accessibility


10. Future extensions influenced by this design

The chosen architecture intentionally leaves room for:

No changes are needed to the core model to support these; they can be layered on top in subsequent phases.

11. The checklist. Make up the items of the planned tasks below. When they are completed, mark them as completed rather than delete them. Periodically fill in new scheduled tasks. This checklist always shows the stage of implementation and the logic of previous actions.