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 VocabularyData from "../components/VocabularyData"; import { IVocab, IReviewCard, vocabToReviewCard, reviewQTypeToStr, VocabType } from "../models/vocab"; import { ReviewType, IReviewMetadata } from "../models/review"; //@ts-ignore import lev from "js-levenshtein"; import { LEVENSHTEIN_MAX_DISTANCE, MAX_ERROR_THRESHOLD, REVIEW_HELP_MOD } from "../config"; import { BACKEND_URL } from "../config"; import { Queue } from "../utils/queue"; interface IProps { levelId?: number; vocabByLevel?: (level: number) => Promise; vocabByQueue?: () => Promise; updateDoneLevels?: (id: string) => void; 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; showHelp: boolean; 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; setShowHelp: (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]; // Track the start of a session fetch(`${BACKEND_URL}/api/tracker`, { headers: new Headers({ "Content-Type": "application/json", }), method: "POST", body: JSON.stringify({ session: window.sessionStorage.getItem("tracker_session"), event: "START_LEARNING", }), }); getVocab().then((res: IVocab[]) => { // Check if we received any vocabulary if (res.length === 0) { // TODO: Replace with a snackbar alert("Du hast noch keine Vokabeln in der Review Queue"); // Reset the button state this.props.drawerButtonState(true); this.props.history.push("/dashboard"); return; } // 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(); // Track the cancellation of a learning session fetch(`${BACKEND_URL}/api/tracker`, { headers: new Headers({ "Content-Type": "application/json", }), method: "POST", body: JSON.stringify({ session: window.sessionStorage.getItem("tracker_session"), event: "CANCEL_LEARNING", }), }); // Show the drawer button again this.props.drawerButtonState(true); this.props.history.push("/dashboard"); } closeHelp = () => { this.props.setShowHelp(false); } increaseMeta = (correct: number, wrong: number): IReviewMetadata => { const { metadata } = this.props; return { wrong: metadata.wrong + wrong, correct: metadata.correct + correct, }; } // 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) => lev(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); 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); // If we're reviewing a level, then that level should be // marked as complete if (this.props.reviewType === ReviewType.LEVEL) { // NOTE: We can ensure that both updateDoneLevels and // levelId are attributes of this.props, since // reviewType === ReviewType.LEVEL requires those. //@ts-ignore this.props.updateDoneLevels(this.props.levelId); } // 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); } else if (this.error_data[current.id] % REVIEW_HELP_MOD === 0) { // Help the user this.props.setShowHelp(true); } this.props.setReview(this.props.current, this.increaseMeta(0, 1)); this.props.setPopover(true, "Das war nicht richtig", "red", "white"); } } helpDialog = () => { // Find the vocabulary // TODO: if (!vocab) const vocab = this.vocab.find(el => el.id === this.props.current.id) as IVocab; return Wiederholung von {vocab.latin.grundform} ; } render() { if (this.props.loading) { return
; } const { question, qtype } = this.props.current; const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`; // NOTE: This assumes that adverbs get mapped to only 1 card, // while every other type of vocabulary gets mapped to // exactly 3 cards each. // 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. // NOTE: The arrow function needs the ': number' annotation as TS // will then deduce the type of acc and curr to be '1|3', // leads to compiler warnings, as a, b element {1, 3} is not // element {1, 3}. // NOTE: We need to check if this.vocab is empty because otherwise // the '.reduce' call will throw and crash the entire // application. // TOFIX: This is pure garbage. Fix this const numCards = this.vocab.length > 0 ? this.vocab.map((vocab): number => { if (vocab.type === VocabType.ADVERB) { return 1; } else { return 3; } }).reduce((acc, curr) => acc + curr) : 0; 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}
{ this.props.showHelp ? ( this.helpDialog() ) : undefined } Willst du die Wiederholung abbrechen? Wenn du jetzt abbricht, dann geht dein in dieser Wiederholung gesammelte Fortschritt verloren.
; } } ); export default ReviewPageWithRouter;