Skip to content

Commit

Permalink
Allow user to select training letters
Browse files Browse the repository at this point in the history
  • Loading branch information
bovee committed May 13, 2024
1 parent e0dfe56 commit 3f1d905
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 73 deletions.
11 changes: 2 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Carousel } from '@mantine/carousel';
import { Button, Center, Container, Flex, Switch } from '@mantine/core';
import { Button, Center, Container, Flex } from '@mantine/core';
import { useCallback, useEffect, useState } from 'react';
import { useLocalStorage } from 'usehooks-ts';

Expand Down Expand Up @@ -29,16 +29,11 @@ export default function App(props: { audio: Audio; keyer: Keyer }) {
const [mobileStart, setMobileStart] = useState(
'ontouchstart' in document.documentElement,
);
const [station, setStation] = useLocalStorage('station', 'test');
const [station] = useLocalStorage('station', 'test');
const [currentGuess, setCurrentGuess] = useState(['', 0]);
const [currentMessage, setCurrentMessage] = useState('');
const [keyType] = useLocalStorage('key-type', 'straight');

const keyTest = () => {
setCurrentMessage('hello');
props.keyer.keyPartnerMessage('hello');
};

const handleKeypress = useCallback(
letter => {
if (station === 'copy' || station === 'rxPractice') {
Expand Down Expand Up @@ -186,8 +181,6 @@ export default function App(props: { audio: Audio; keyer: Keyer }) {
</Container>
<br />
<Container size="sm">{key}</Container>
<br />
<Button onClick={() => keyTest()} />
</>
);
}
111 changes: 72 additions & 39 deletions src/Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { Audio } from './audio';
import { Keyer } from './keyer';
import { PERSONAS } from './data';

export const LCWO_LETTERS = 'kmuresnaptlwi.jz=foy,vg5/q92h38b?47c1d60x';
const FINLEY_LETTERS = 'kmrsuaptlowi.njef0y,vg5/q9zh38b?427c1d6x';

export function Display(props: {
audio: Audio;
keyer: Keyer;
Expand Down Expand Up @@ -54,9 +57,11 @@ export function Display(props: {
const startTraining = useCallback(() => {
let testMessage = '';

for (let i=0; i < 25; i++) {
for (let i = 0; i < 25; i++) {
if (i && i % 5 === 0) testMessage += ' ';
testMessage += progress.training.charAt(Math.floor(Math.random() * progress.training.length));
testMessage += progress.training.charAt(
Math.floor(Math.random() * progress.training.length),
);
}

setCurrentMessage(testMessage);
Expand All @@ -66,14 +71,20 @@ export function Display(props: {
const gradeGuess = useCallback(
(chr: string): number => {
let score = 0;
if (!(chr in currentLesson)) currentLesson[chr] = { total: 0, correct: 0, correctTimes: [], wrongsGuesses: []};
if (!(chr in currentLesson))
currentLesson[chr] = {
total: 0,
correct: 0,
correctTimes: [],
wrongGuesses: [],
};
currentLesson[chr].total += 1;
if (currentGuess && currentGuess[0] === chr) {
if (currentGuess[0] && currentGuess[0] === chr) {
currentLesson[chr].correct += 1;
const delay = 1000 * (keyer.currentTime - currentGuess[1]);
const delay = Math.round(1000 * (keyer.currentTime - currentGuess[1]));
currentLesson[chr].correctTimes.push(delay);
score = 1;
} else if (currentGuess && currentGuess[0] !== chr) {
} else if (currentGuess[0] && currentGuess[0] !== chr) {
score = -1;
currentLesson[chr].wrongGuesses.push(currentGuess[0]);
} else {
Expand All @@ -92,38 +103,44 @@ export function Display(props: {
!progress.daily.length ||
progress.daily[progress.daily.length - 1][0] !== today
)
progress.daily.push([today, 0]);
progress.daily.push([today, 0]);
progress.daily[progress.daily.length - 1][1] += 1;

// update per-letter progress
let correct = 0;
let total = 0;
for (const [letter, data] of Object.entries(currentLesson)) {
correct += data.corrent;
correct += data.correct;
total += data.total;
if (!(letter in progress.letters)) progress.letters[letter] = {
recentSpeed: [],
wrongGuesses: [],
totals: [],
corrects: [],
wrongs: [],
};
if (!(letter in progress.letters))
progress.letters[letter] = {
recentSpeed: [],
wrongGuesses: [],
totals: [],
corrects: [],
wrongs: [],
};
for (const time of data.correctTimes) {
progress.letters[letter].recentSpeed.push(time);
if (progress.letters[letter].recentSpeed.length > 20) progress.letters[letter].recentSpeed.shift();
if (progress.letters[letter].recentSpeed.length > 20)
progress.letters[letter].recentSpeed.shift();
}
for (const guess of data.wrongGuesses) {
progress.letters[letter].wrongGuesses.push(guess);
if (progress.letters[letter].wrongGuesses.length > 20) progress.letters[letter].wrongGuesses.shift();
if (progress.letters[letter].wrongGuesses.length > 20)
progress.letters[letter].wrongGuesses.shift();
}

// per-lesson statistics
progress.letters[letter].totals.push(data.total);
if (progress.letters[letter].totals.length > 10) progress.letters[letter].totals.shift();
progress.letters[letter].corrects.push(data.corrects);
if (progress.letters[letter].corrects.length > 10) progress.letters[letter].corrects.shift();
if (progress.letters[letter].totals.length > 10)
progress.letters[letter].totals.shift();
progress.letters[letter].corrects.push(data.correct);
if (progress.letters[letter].corrects.length > 10)
progress.letters[letter].corrects.shift();
progress.letters[letter].wrongs.push(data.wrongGuesses.length);
if (progress.letters[letter].wrongs.length > 10) progress.letters[letter].wrongs.shift();
if (progress.letters[letter].wrongs.length > 10)
progress.letters[letter].wrongs.shift();
}

const perCorrect = correct / total;
Expand All @@ -135,7 +152,8 @@ export function Display(props: {
useEffect(() => {
keyer.attach((chr, primary) => {
if (!chr) return;
// handle user guesses

let color = primary ? 'black' : 'grey';
switch (station) {
case 'copy':
if (primary) {
Expand All @@ -153,16 +171,24 @@ export function Display(props: {
const perCorrect = updateProgress();
let extraButton = <span />;
if (perCorrect > 0.9) {
let letter = '';
// FIXME!!!!
extraButton = (<Button
size="compact-xs"
onClick={() => {
progress.training += letter;
setProgress(progress);
}}>
Add the letter {letter.toUpperCase()}
</Button>);
const currentLetters = new Set(progress.training);
for (const letter of LCWO_LETTERS) {
if (!currentLetters.has(letter)) {
extraButton = (
<Button
size="compact-xs"
onClick={() => {
progress.training += letter;
// TODO: this should also close the notification?
setProgress(progress);
}}
>
Add the letter {letter.toUpperCase()}
</Button>
);
break;
}
}
}
notifications.show({
icon: PERSONAS.elmer.icon,
Expand All @@ -171,10 +197,12 @@ export function Display(props: {
radius: 'lg',
title: PERSONAS.elmer.name,
autoClose: false,
message: (<Group gap="xl">
You did it! {Math.round(100 * perCorrect)}% right.
{ extraButton }
</Group>),
message: (
<Group gap="xl">
You did it! {Math.round(100 * perCorrect)}% right.
{extraButton}
</Group>
),
});
}

Expand All @@ -184,9 +212,8 @@ export function Display(props: {
);
if (currentMessage) setCurrentMessage(currentMessage.slice(1));

let color = 'grey';
if (chr !== ' ') {
let score = gradeGuess(chr);
const score = gradeGuess(chr);
if (score === 1) color = 'blue';
if (score === -1) color = 'red';
}
Expand All @@ -195,7 +222,7 @@ export function Display(props: {
updateDisplay(chr, color);
break;
default:
updateDisplay(chr, primary ? 'black' : 'grey');
updateDisplay(chr, color);
}
});

Expand All @@ -212,11 +239,17 @@ export function Display(props: {
currentGuess,
currentLesson,
currentMessage,
gradeGuess,
keyer,
progress,
setCurrentGuess,
setCurrentLesson,
setCurrentMessage,
setProgress,
startTraining,
station,
updateProgress,
updateDisplay,
]);

return (
Expand Down
60 changes: 41 additions & 19 deletions src/Stations.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
import { SegmentedControl, Slider } from '@mantine/core';
import { Checkbox, Flex, SegmentedControl } from '@mantine/core';
import { useLocalStorage } from 'usehooks-ts';

const LCWO_LETTERS = 'kmuresnaptlwi.jz=foy,vg5/q92h38b?47c1d60x';
const HOLECEK_DROID_LETTERS =
'etimansorkdugwhpxbflvczjqy1234567890.,:?\'-/()"=+@';
const FINLEY_LETTERS = 'kmrsuaptlowi.njef0y,vg5/q9zh38b?427c1d6x';
import { LCWO_LETTERS } from './Display';

export function StationPane() {
let [station, setStation] = useLocalStorage('station', 'test');
const [progress, setProgress] = useLocalStorage('learning-progress', {
training: 'kmur',
letters: {},
daily: [],
});
const [station, setStation] = useLocalStorage('station', 'test');

let description = '';
let controls = <></>;
if (station === 'test') description = 'Practice sending with the keys.';
if (station === 'copy') description = 'Practice receiving copy.';
if (station === 'copy') {
description = 'Practice receiving copy.';
const boxes = [];
for (const letter of LCWO_LETTERS) {
boxes.push(
<Checkbox
key={letter}
label={letter}
checked={progress.training.indexOf(letter) > -1}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
const loc = progress.training.indexOf(letter);
console.log(evt.currentTarget.checked, loc);
if (evt.currentTarget.checked && loc === -1) {
progress.training += letter;
} else if (!evt.currentTarget.checked) {
progress.training =
progress.training.slice(0, loc) +
progress.training.slice(loc + 1);
}
setProgress(progress);
}}
/>,
);
}
controls = (
<Flex gap="md" wrap="wrap">
{' '}
{boxes}{' '}
</Flex>
);
}
if (station === 'listen') description = 'Listen to text.';
if (station === 'txPractice') description = 'Initiate a QSO.';
if (station === 'rxPractice') description = 'Respond to a QSO.';

const marks = LCWO_LETTERS.split('').map((letter, ix) => ({
value: ix,
label: letter,
}));

return (
<>
<SegmentedControl
Expand All @@ -37,12 +64,7 @@ export function StationPane() {
/>
<br />
{description}
<Slider
defaultValue={0}
max={40}
label={val => marks.find(mark => mark.value === val)!.label}
marks={marks}
/>
{controls}
</>
);
}
2 changes: 1 addition & 1 deletion src/keyer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class Keyer {

keyLetter(letter: string, primary: boolean = true) {
if (this.timeoutGap) clearTimeout(this.timeoutGap);
let code = MORSE_MAP[letter.toLowerCase()]
let code = MORSE_MAP[letter.toLowerCase()];
if (letter === ' ') code = ' ';
const delay = this.audio.key(
code,
Expand Down
22 changes: 17 additions & 5 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import '@mantine/notifications/styles.css';

import React from 'react';
import ReactDOM from 'react-dom/client';
import { localStorageColorSchemeManager, Button, Group, MantineProvider } from '@mantine/core';
import {
localStorageColorSchemeManager,
Button,
Group,
MantineProvider,
} from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import { notifications } from '@mantine/notifications';

Expand All @@ -17,6 +22,9 @@ const volume = parseFloat(localStorage.getItem('volume') || '1');
const wpm = parseInt(localStorage.getItem('wpm') || '25');
const farnsworth = parseInt(localStorage.getItem('farnsworth') || '5');

if (new URLSearchParams(document.location.search).has('clearLocalStorage'))
window.localStorage.clear();

const audio = new Audio(volume);
const keyer = new Keyer(audio, wpm, farnsworth);
window.keyer = keyer;
Expand All @@ -32,10 +40,14 @@ notifications.show({
radius: 'lg',
title: PERSONAS.elmer.name,
autoClose: false,
message: (<Group gap="xl">
Welcome! I'm here to help you learn morse code.
<Button size="compact-xs" onClick={() => console.log('Tour')}>Take a tour</Button>
</Group>),
message: (
<Group gap="xl">
Welcome! I&apos;m here to help you learn morse code.
<Button size="compact-xs" onClick={() => console.log('Tour')}>
Take a tour
</Button>
</Group>
),
});

ReactDOM.createRoot(document.getElementById('root')).render(
Expand Down

0 comments on commit 3f1d905

Please sign in to comment.