feat: Implement a review queue

This commit is contained in:
Alexander Polynomdivision 2018-09-15 16:51:21 +02:00
parent 12993570fe
commit 1200e0e3eb
2 changed files with 79 additions and 34 deletions

View File

@ -39,12 +39,12 @@ export interface IVocab {
type: VocabType; type: VocabType;
latin: INomenData | IVerbData | IAdjektivData; latin: INomenData | IVerbData | IAdjektivData;
// This number is lesson specific // This number is unique across all vocabulary items
id: number; id: number;
}; };
// What kind of question should be answered // What kind of question should be answered
export enum IReviewQType { export enum ReviewQType {
GERMAN, GERMAN,
NOMEN_GENITIV, NOMEN_GENITIV,
@ -63,9 +63,36 @@ export interface IReviewCard {
// If a question can have multiple answers // If a question can have multiple answers
answers: string[]; answers: string[];
qtype: IReviewQType; qtype: ReviewQType;
// Identical to its corresponding IVocab item
id: number;
}; };
export function reviewQTypeToStr(type: ReviewQType): string {
switch (type) {
case ReviewQType.GERMAN:
return "Übersetzung";
case ReviewQType.NOMEN_GENITIV:
return "Genitiv";
case ReviewQType.NOMEN_GENUS:
return "Genus";
case ReviewQType.ADJ_NOM_A:
// TODO
return "Nominativ A";
case ReviewQType.ADJ_NOM_B:
// TODO
return "Nominativ B";
case ReviewQType.VERB_PRAESENS:
return "1. Person Präsens";
case ReviewQType.VERB_PERFEKT:
return "1. Person Perfekt";
case ReviewQType.VERB_PPP:
// TODO
return "PPP";
}
}
// Turn a vocabulaty item into a series of questions about the item // Turn a vocabulaty item into a series of questions about the item
export function vocabToReviewCard(vocab: IVocab): IReviewCard[] { export function vocabToReviewCard(vocab: IVocab): IReviewCard[] {
switch (vocab.type) { switch (vocab.type) {
@ -75,24 +102,27 @@ export function vocabToReviewCard(vocab: IVocab): IReviewCard[] {
// Latin -> German // Latin -> German
question: latin.grundform, question: latin.grundform,
answers: vocab.german, answers: vocab.german,
qtype: IReviewQType.GERMAN, qtype: ReviewQType.GERMAN,
id: vocab.id,
}, { }, {
// Latin -> Genitiv // Latin -> Genitiv
question: latin.grundform, question: latin.grundform,
answers: [latin.genitiv], answers: [latin.genitiv],
qtype: IReviewQType.NOMEN_GENITIV, qtype: ReviewQType.NOMEN_GENITIV,
id: vocab.id,
}, { }, {
// Latin -> Genus // Latin -> Genus
question: latin.grundform, question: latin.grundform,
answers: [latin.genus], answers: [latin.genus],
qtype: IReviewQType.NOMEN_GENUS, qtype: ReviewQType.NOMEN_GENUS,
id: vocab.id,
}]; }];
default: default:
return []; return [];
} }
} }
export const typeToPoints = (VocabType type) => { export function typeToPoints(type: VocabType) {
switch (type) { switch (type) {
// Nomen: 2P + 1 (Wenn richtig) // Nomen: 2P + 1 (Wenn richtig)
// //

View File

@ -7,16 +7,17 @@ import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Popover from "@material-ui/core/Popover"; import Popover from "@material-ui/core/Popover";
import Paper from "@material-ui/core/Paper";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import { IVocab, ReviewMode, VocabType } from "../models/vocab"; import { IVocab, IReviewCard, vocabToReviewCard, reviewQTypeToStr } from "../models/vocab";
import { ReviewType } from "../models/review"; import { ReviewType } from "../models/review";
import { levW } from "../algorithms/levenshtein"; import { levW } from "../algorithms/levenshtein";
import { LEVENSHTEIN_MAX_DISTANCE } from "../config"; import { LEVENSHTEIN_MAX_DISTANCE } from "../config";
import { Queue } from "../utils/queue";
interface IProps { interface IProps {
levelId?: number; levelId?: number;
vocabByLevel?: (level: number) => IVocab[]; vocabByLevel?: (level: number) => IVocab[];
@ -29,7 +30,7 @@ interface IProps {
interface IState { interface IState {
input: string; input: string;
current: number; current: IReviewCard;
toSummary: boolean; toSummary: boolean;
@ -40,23 +41,13 @@ interface IState {
export default class ReviewPage extends React.Component<IProps, IState> { export default class ReviewPage extends React.Component<IProps, IState> {
private vocab: IVocab[] = []; private vocab: IVocab[] = [];
private reviewQueue: Queue<IReviewCard> = new Queue();
// Used for positioning the popover // Used for positioning the popover
private buttonRef: HTMLButtonElement; private buttonRef: HTMLButtonElement;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = {
input: "",
current: 0,
toSummary: false,
popoverOpen: false,
popoverText: "",
popoverColor: "red",
};
// Hide the drawer button // Hide the drawer button
this.props.drawerButtonState(false); this.props.drawerButtonState(false);
@ -80,26 +71,44 @@ export default class ReviewPage extends React.Component<IProps, IState> {
break; break;
} }
// Turn the vocabulary into IReviewCards and queue them
this.vocab.forEach((vocab) => {
vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue);
});
this.state = {
input: "",
current: this.reviewQueue.dequeue(),
toSummary: false,
popoverOpen: false,
popoverText: "",
popoverColor: "red",
};
} }
currentVocab = () => { currentVocab = (id: number) => {
return this.vocab[this.state.current]; return this.vocab.find((el) => el.id === this.state.current.id);
} }
checkInput = () => { checkInput = () => {
const current = this.currentVocab();
// Check if the given answer is somewhere in the german words // Check if the given answer is somewhere in the german words
const { input } = this.state; const { input } = this.state;
const german = current.german.map((str) => str.toLowerCase());
const dists = german.map((ger) => levW(input.toLowerCase(), ger)); // Map all possible answers to lowercase ( => ignore casing)
const answers = this.state.current.answers.map((el) => el.toLowerCase());
// Calculate the distances to all possible answers
const dists = answers.map((el) => levW(input.toLowerCase(), el));
// Find the lowest distance
const minDist = Math.min(...dists); const minDist = Math.min(...dists);
// Check if the user's answer was correct // Check if the user's answer was correct
if (minDist === 0) { if (minDist === 0) {
// TODO: Show it's correct? // TODO: Show it's correct?
// Show the next vocab word // Show the next vocab word
if (this.state.current + 1 >= this.vocab.length) { if (this.reviewQueue.size() === 0) {
// TODO: Set some data that the summary screen will show // TODO: Set some data that the summary screen will show
this.setState({ this.setState({
toSummary: true, toSummary: true,
@ -107,7 +116,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
} else { } else {
// Increase the vocab // Increase the vocab
this.setState({ this.setState({
current: this.state.current + 1, current: this.reviewQueue.dequeue(),
input: "", input: "",
}); });
} }
@ -128,6 +137,8 @@ export default class ReviewPage extends React.Component<IProps, IState> {
} }
render() { render() {
const { question, qtype } = this.state.current;
const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`;
return <div> return <div>
{ {
this.state.toSummary ? ( this.state.toSummary ? (
@ -140,7 +151,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
<CardContent> <CardContent>
<Grid container direction="column"> <Grid container direction="column">
<Typography variant="display2"> <Typography variant="display2">
{this.currentVocab().latin.grundform} {questionTitle}
</Typography> </Typography>
<TextField <TextField
margin="normal" margin="normal"
@ -158,11 +169,11 @@ export default class ReviewPage extends React.Component<IProps, IState> {
open={this.state.popoverOpen} open={this.state.popoverOpen}
anchorOrigin={{ anchorOrigin={{
vertical: "center", vertical: "center",
horizontal: "left" horizontal: "center"
}} }}
transformOrigin={{ transformOrigin={{
vertical: "bottom", vertical: "bottom",
horizontal: "left" horizontal: "center"
}} }}
anchorEl={this.buttonRef} anchorEl={this.buttonRef}
onClose={() => this.setState({ onClose={() => this.setState({
@ -175,11 +186,15 @@ export default class ReviewPage extends React.Component<IProps, IState> {
color: "white" color: "white"
} }
}}> }}>
<Typography variant="button" color="inherit"> <Typography
variant="button"
color="inherit">
{this.state.popoverText} {this.state.popoverText}
</Typography> </Typography>
</Popover> </Popover>
<Button onClick={this.checkInput} buttonRef={node => this.buttonRef = node}> <Button
onClick={this.checkInput}
buttonRef={node => this.buttonRef = node}>
Prüfen Prüfen
</Button> </Button>
</Grid> </Grid>