import * as React from "react"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import TextField from "@material-ui/core/TextField"; import Grid from "@material-ui/core/Grid"; import Button from "@material-ui/core/Button"; import Typography from "@material-ui/core/Typography"; import Popover from "@material-ui/core/Popover"; import LinearProgress from "@material-ui/core/LinearProgress"; import CircularProgress from "@material-ui/core/CircularProgress"; import Paper from "@material-ui/core/Paper"; import Tooltip from "@material-ui/core/Tooltip"; import Dialog from "@material-ui/core/Dialog"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogContentText from "@material-ui/core/DialogContentText"; import DialogTitle from "@material-ui/core/DialogTitle"; import CloseIcon from "@material-ui/icons/Close"; import { withRouter } from "react-router-dom"; import { IVocab, IReviewCard, vocabToReviewCard, reviewQTypeToStr } from "../models/vocab"; import { ReviewType, IReviewMetadata } from "../models/review"; import { levW } from "../algorithms/levenshtein"; import { LEVENSHTEIN_MAX_DISTANCE, MAX_ERROR_THRESHOLD } from "../config"; import { Queue } from "../utils/queue"; interface IProps { levelId?: number; vocabByLevel?: (level: number) => Promise; vocabByQueue?: () => Promise; setLastReview: (meta: IReviewMetadata, sm2: any, delta: number) => void; reviewType: ReviewType; history: any; dialogOpen: boolean; loading: boolean; vocab: IVocab[]; current: IReviewCard; metadata: IReviewMetadata; popoverOpen: boolean; popoverText: string; popoverColor: string; popoverTextColor: string; setReviewDialog: (state: boolean) => void; setSummary: (state: boolean) => void; setPopover: (state: boolean, text: string, color: string, textColor: string) => void; drawerButtonState: (state: boolean) => void; setReview: (curent: IReviewCard, meta: IReviewMetadata) => void; setLoading: (state: boolean) => void; } const ReviewPageWithRouter = withRouter( class ReviewPage extends React.Component { private vocab: IVocab[] = []; private reviewQueue: Queue = new Queue(); // Used for positioning the popover private buttonRef: HTMLButtonElement; private inputRef: HTMLInputElement; // How often a group has been wrongly answered // (Mapping: Vocab Id -> amount of times it was wrongly answered) private error_data = {}; // Mapping: Vocab Id -> Correctly answered private sm2_metadata: any = {}; private score_delta = 0; componentDidMount() { // Hide the drawer button this.props.drawerButtonState(false); // Show a loading spinner this.props.setLoading(true); // Get the correct vocabulary const { reviewType, vocabByLevel, levelId, vocabByQueue } = this.props; // Just to make TSC shut up const noopPromise = () => { return new Promise((res, rej) => { rej([]); }); }; const vocabByLevelW = vocabByLevel || noopPromise; const vocabByQueueW = vocabByQueue || noopPromise; const getVocab = { [ReviewType.LEVEL]: () => vocabByLevelW(levelId), [ReviewType.QUEUE]: () => vocabByQueueW(), }[reviewType]; getVocab().then((res: IVocab[]) => { // Stop the loading this.props.setLoading(false); this.vocab = res; // Convert the vocab items into review queue cards res.forEach(vocab => { // Set the error data for the group this.error_data[vocab.id] = 0; vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue); }); // Set the initial vocabulary card this.props.setReview(this.reviewQueue.dequeue(), { correct: 0, wrong: 0, }); }); } openDialog = () => { this.props.setReviewDialog(true); } closeDialog = () => { this.props.setReviewDialog(false); } cancelReview = () => { this.closeDialog(); // Show the drawer button again this.props.drawerButtonState(true); this.props.history.push("/dashboard"); } increaseMeta = (correct: number, wrong: number): IReviewMetadata => { const { metadata } = this.props; return { wrong: metadata.wrong + wrong, correct: metadata.correct + correct, }; } vocabFromId = (id: number): IVocab | {} => { return this.vocab.find((el) => el.id === this.props.current.id); } // When a vocabulary item has been answered, we need to update // the group's SuperMemo2-data. // We update based on the following State-Machine: (C = Correct; W = Wrong) // Answer | Group | New group // | | state // -------+-------+---------- // C | C | C // C | W | W // W | C | W // W | W | W // (1)C | - | C // (2)W | - | W // Args: // @correct: Was the answer given correct? updateGroupSM2 = (correct: boolean) => { const { id } = this.props.current; switch (correct) { case true: // Case (1) if (!(id in this.sm2_metadata)) { this.sm2_metadata[id] = true; break; } switch (this.sm2_metadata[id]) { case true: this.sm2_metadata[id] = true; break; case false: this.sm2_metadata[id] = false; break; } break; case false: // We don't need to explicitly catch case (2), as we set // anything, that was incorrectly answered to false. this.sm2_metadata[id] = false; break; } } checkInput = () => { // Check if the given answer is somewhere in the german words const input = this.inputRef.value || ""; const { current } = this.props; // Map all possible answers to lowercase ( => ignore casing) const answers = 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); // Check if the user's answer was correct if (minDist === 0) { this.updateGroupSM2(true); // TODO: Is this the correct amount of points? this.score_delta += 1; // Empty the input field this.inputRef.value = ""; // TODO: Show it's correct? // Show the next vocab word if (this.reviewQueue.size() === 0) { // Update the metadata const newMeta = this.increaseMeta(1, 0); this.props.setReview(current, newMeta); this.props.setLastReview(newMeta, this.sm2_metadata, this.score_delta); this.props.setLoading(true); // Show the drawer button again this.props.drawerButtonState(true); // Go to the summary screen this.props.history.push("/review/summary"); } else { // Update the metadata this.props.setReview(this.reviewQueue.dequeue(), this.increaseMeta(1, 0)); } } else if (minDist <= LEVENSHTEIN_MAX_DISTANCE) { this.props.setPopover(true, "Das war fast richtig", "yellow", "black"); } else { // Update the metadata this.updateGroupSM2(false); // Update the error data this.error_data[current.id] += 1; if (this.error_data[current.id] <= MAX_ERROR_THRESHOLD) { // Read the vocabulary group to the queue // Find the vocabulary item // TODO: if(!vocab) const vocab = this.vocab.find(el => el.id === current.id); // Re-add the vocabulary group // NOTE: We don't need to force a re-render, as the state // will be updated since we need to show the popover. vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue); } // TODO: Is this the correct amount this.score_delta -= 1; this.props.setReview(this.props.current, this.increaseMeta(0, 1)); this.props.setPopover(true, "Das war nicht richtig", "red", "white"); } } render() { if (this.props.loading) { return
; } const { question, qtype } = this.props.current; const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`; // TODO/NOTE: This assumes that each vocabulary item gets mapped to // exactly 3 review cards // NOTE: The 'numCards === 0 ?' is neccessary as (for some reasdn) numCards // starts of by being 0, which results in progress === -Inifinity. // That looks weird as the user sees the progressbar jump. const numCards = this.vocab.length * 3; const progress = numCards === 0 ? ( 0 ) : ( 100 * (numCards - (this.reviewQueue.size() + 1)) / numCards ); return
{questionTitle}
this.inputRef = node} onKeyPress={(ev) => { // Allow checking of the answer by pressing Enter if (ev.key === "Enter") this.checkInput(); }} /> this.props.setPopover(false, "", "", "")} PaperProps={{ style: { backgroundColor: this.props.popoverColor, padding: 10, color: this.props.popoverTextColor, } }}> {this.props.popoverText}
Willst du die Wiederholung abbrechen? Wenn du jetzt abbricht, dann geht dein in dieser Wiederholung gesammelte Fortschritt verloren.
; } } ); export default ReviewPageWithRouter;