From 30ba15fd9a9f0b59e4664dfed3325670fec6b368 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Sun, 11 Jun 2023 14:27:49 -0500 Subject: [PATCH] review works! --- src-tauri/src/srs.rs | 15 +++- src-tauri/src/utils.rs | 4 +- src/components/KanjiDisplay.tsx | 2 +- src/components/KanjiList.tsx | 8 +- src/components/utils/GradeBadge.tsx | 28 ------- src/components/utils/LevelBadge.tsx | 33 ++++++++ src/data/srslevels.json | 69 ++++++++++++++++ src/{types/Kanji.ts => lib/kanji.ts} | 0 src/lib/srs.ts | 57 +++++++++++++ src/panes/KanjiPane.tsx | 2 +- src/panes/SrsReviewPane.module.scss | 3 - src/panes/SrsReviewPane.tsx | 117 +++++++++++++++++++-------- src/types/Srs.ts | 19 ----- 13 files changed, 266 insertions(+), 91 deletions(-) delete mode 100644 src/components/utils/GradeBadge.tsx create mode 100644 src/components/utils/LevelBadge.tsx create mode 100644 src/data/srslevels.json rename src/{types/Kanji.ts => lib/kanji.ts} (100%) create mode 100644 src/lib/srs.ts delete mode 100644 src/types/Srs.ts diff --git a/src-tauri/src/srs.rs b/src-tauri/src/srs.rs index f7d6e4b..25bd89f 100644 --- a/src-tauri/src/srs.rs +++ b/src-tauri/src/srs.rs @@ -3,7 +3,10 @@ use std::time::Duration; use sqlx::{Row, SqlitePool}; use tauri::State; -use crate::{kanji::KanjiDb, utils::Ticks}; +use crate::{ + kanji::KanjiDb, + utils::{Ticks, TICK_MULTIPLIER}, +}; pub struct SrsDb(pub SqlitePool); @@ -217,6 +220,8 @@ pub async fn generate_review_batch( pub async fn update_srs_item( srs_db: State<'_, SrsDb>, item_id: u32, + delay: i64, + new_grade: u32, correct: bool, ) -> Result<(), String> { let (success, failure) = match correct { @@ -224,17 +229,23 @@ pub async fn update_srs_item( false => (0, 1), }; + // Kanji.Interface/ViewModels/Partial/Srs/SrsReviewViewModel.cs:600 + sqlx::query( r#" UPDATE SrsEntrySet SET SuccessCount = SuccessCount + ?, - FailureCount = FailureCount + ? + FailureCount = FailureCount + ?, + NextAnswerDate = NextAnswerDate + ?, + CurrentGrade = ? WHERE ID = ? "#, ) .bind(success) .bind(failure) + .bind(delay * TICK_MULTIPLIER) + .bind(new_grade) .bind(item_id) .execute(&srs_db.0) .await diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index a1f7775..47151a9 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -10,6 +10,8 @@ use sqlx::{ Decode, Encode, Sqlite, Type, }; +pub const TICK_MULTIPLIER: i64 = 1_000_000_000; + #[derive(Clone, Copy)] pub struct Ticks(pub i64); @@ -37,7 +39,7 @@ impl Into for Ticks { impl From for Ticks { fn from(value: Duration) -> Self { - Ticks(value.as_secs() as i64 * 1_000_000_000) + Ticks(value.as_secs() as i64 * TICK_MULTIPLIER) } } diff --git a/src/components/KanjiDisplay.tsx b/src/components/KanjiDisplay.tsx index 51551d0..7f07299 100644 --- a/src/components/KanjiDisplay.tsx +++ b/src/components/KanjiDisplay.tsx @@ -52,7 +52,7 @@ export default function KanjiDisplay({ kanjiCharacter }: KanjiDisplayProps) { srsPart = ( -

This character is being tracked in SRS!

+

You are learning this item!

{isValid(nextAnswerDate) && (

diff --git a/src/components/KanjiList.tsx b/src/components/KanjiList.tsx index 95ae388..79bfb79 100644 --- a/src/components/KanjiList.tsx +++ b/src/components/KanjiList.tsx @@ -3,11 +3,11 @@ import classNames from "classnames"; import { Link } from "react-router-dom"; import { Badge, Grid, GridItem } from "@chakra-ui/layout"; import styles from "./KanjiList.module.scss"; -import { Kanji } from "../types/Kanji"; +import { Kanji } from "../lib/kanji"; import { Input, Spinner } from "@chakra-ui/react"; import { useCallback, useEffect, useState } from "react"; import SearchBar from "./SearchBar"; -import GradeBadge from "./utils/GradeBadge"; +import LevelBadge from "./utils/LevelBadge"; export interface KanjiListProps { kanjiList: Kanji[]; @@ -57,6 +57,8 @@ export function KanjiList({ const renderKanjiItem = (kanji: Kanji, active: boolean) => { const className = classNames(styles["kanji-link"], active && styles["kanji-link-active"]); + if (kanji.srs_info) console.log("kanji", kanji); + return ( @@ -65,7 +67,7 @@ export function KanjiList({ {kanji.meanings[0].meaning} - + #{kanji.most_used_rank} common diff --git a/src/components/utils/GradeBadge.tsx b/src/components/utils/GradeBadge.tsx deleted file mode 100644 index c55eec3..0000000 --- a/src/components/utils/GradeBadge.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Badge } from "@chakra-ui/react"; - -export interface GradeBadgeProps { - grade?: number; -} - -export default function GradeBadge({ grade }: GradeBadgeProps) { - if (!grade) return null; - - const badgeInfo = badgeMap.get(grade); - if (!badgeInfo) return null; - - const [letter, colorScheme] = badgeInfo; - - return {letter}; -} - -const badgeMap = new Map([ - [8, [<>★, "green"]], - [7, ["A2", "blue"]], - [6, ["A1", "blue"]], - [5, ["B2", "yellow"]], - [4, ["B1", "yellow"]], - [3, ["C2", "orange"]], - [2, ["C1", "orange"]], - [1, ["D2", "red"]], - [0, ["D1", "red"]], -]); diff --git a/src/components/utils/LevelBadge.tsx b/src/components/utils/LevelBadge.tsx new file mode 100644 index 0000000..4fe4444 --- /dev/null +++ b/src/components/utils/LevelBadge.tsx @@ -0,0 +1,33 @@ +import { Badge } from "@chakra-ui/react"; +import { srsLevels } from "../../lib/srs"; + +export interface LevelBadgeProps { + grade?: number; +} + +export default function LevelBadge({ grade }: LevelBadgeProps) { + if (grade == undefined) return null; + + const levelInfo = srsLevels.get(grade); + if (!levelInfo) return null; + + const { color, name } = levelInfo; + + return ( + + {name} + + ); +} + +const badgeMap = new Map([ + [8, [<>★, "green"]], + [7, ["A2", "blue"]], + [6, ["A1", "blue"]], + [5, ["B2", "yellow"]], + [4, ["B1", "yellow"]], + [3, ["C2", "orange"]], + [2, ["C1", "orange"]], + [1, ["D2", "red"]], + [0, ["D1", "red"]], +]); diff --git a/src/data/srslevels.json b/src/data/srslevels.json new file mode 100644 index 0000000..fcb4030 --- /dev/null +++ b/src/data/srslevels.json @@ -0,0 +1,69 @@ +[ + { + "group": "Set In Stone", + "name": "★", + "value": 8, + "delay": null, + "color": "#1C1C1C" + }, + + { + "group": "Assimilating", + "name": "A2", + "value": 7, + "delay": 10368000, + "color": "#890062" + }, + { + "group": "Assimilating", + "name": "A1", + "value": 6, + "delay": 2592000, + "color": "#890062" + }, + + { + "group": "Bolstering", + "name": "B2", + "value": 5, + "delay": 1209600, + "color": "#5C1696" + }, + { + "group": "Bolstering", + "name": "B1", + "value": 4, + "delay": 604800, + "color": "#5C1696" + }, + + { + "group": "Committing", + "name": "C2", + "value": 3, + "delay": 259200, + "color": "#004E8E" + }, + { + "group": "Committing", + "name": "C1", + "value": 2, + "delay": 86400, + "color": "#004E8E" + }, + + { + "group": "Discovering", + "name": "D2", + "value": 1, + "delay": 28800, + "color": "#1A814D" + }, + { + "group": "Discovering", + "name": "D1", + "value": 0, + "delay": 14400, + "color": "#1A814D" + } +] diff --git a/src/types/Kanji.ts b/src/lib/kanji.ts similarity index 100% rename from src/types/Kanji.ts rename to src/lib/kanji.ts diff --git a/src/lib/srs.ts b/src/lib/srs.ts new file mode 100644 index 0000000..6283869 --- /dev/null +++ b/src/lib/srs.ts @@ -0,0 +1,57 @@ +export interface SrsEntry { + id: number; + associated_kanji: string; + current_grade: number; + meanings: string[]; + readings: string[]; +} + +export interface SrsLevel { + group: string; + name: string; + value: number; + /** In seconds */ + delay: number | null; + color: string; +} + +import srsLevelMap from "../data/srslevels.json"; +export const srsLevels: Map = new Map(srsLevelMap.map((v) => [v.value, v])); + +export interface SrsQuestionGroup { + srsEntry: SrsEntry; + questions: { meaningQuestion: ReviewItem; readingQuestion: ReviewItem }; +} + +export function allQuestionsAnswered(group: SrsQuestionGroup): boolean { + return Object.values(group.questions).every((v) => v.isCorrect != null); +} + +export function isGroupCorrect(group: SrsQuestionGroup): boolean { + return Object.values(group.questions).every((v) => v.isCorrect == true); +} + +export function groupUpdatedLevel(group: SrsQuestionGroup): SrsLevel { + const grade = group.srsEntry.current_grade; + const modifier = isGroupCorrect(group) ? 1 : -1; + + return ( + srsLevels.get(grade + modifier) ?? + // Rip type coercion, but current grade should be pretty much set + (srsLevels.get(grade) as SrsLevel) + ); +} + +export enum ReviewItemType { + MEANING = "MEANING", + READING = "READING", +} + +export interface ReviewItem { + parent: SrsQuestionGroup; + type: ReviewItemType; + challenge: string; + possibleAnswers: string[]; + isCorrect: boolean | null; + timesRepeated: number; +} diff --git a/src/panes/KanjiPane.tsx b/src/panes/KanjiPane.tsx index e3292c1..0cbd81d 100644 --- a/src/panes/KanjiPane.tsx +++ b/src/panes/KanjiPane.tsx @@ -5,7 +5,7 @@ import { Box, Flex, Grid, GridItem, LinkBox, Stack } from "@chakra-ui/layout"; import styles from "./KanjiPane.module.scss"; import { useParams } from "react-router-dom"; import KanjiDisplay from "../components/KanjiDisplay"; -import { Kanji } from "../types/Kanji"; +import { Kanji } from "../lib/kanji"; import { KanjiList } from "../components/KanjiList"; import { useEffect, useState } from "react"; diff --git a/src/panes/SrsReviewPane.module.scss b/src/panes/SrsReviewPane.module.scss index e78ac9f..bb2ee98 100644 --- a/src/panes/SrsReviewPane.module.scss +++ b/src/panes/SrsReviewPane.module.scss @@ -17,9 +17,6 @@ padding: 64px 0; } -.input-box { -} - .incorrect { background-color: rgb(255, 202, 202) !important; } diff --git a/src/panes/SrsReviewPane.tsx b/src/panes/SrsReviewPane.tsx index 1d4d850..649b108 100644 --- a/src/panes/SrsReviewPane.tsx +++ b/src/panes/SrsReviewPane.tsx @@ -4,6 +4,7 @@ import { Input, InputGroup, InputLeftElement, + InputRightElement, Progress, Spinner, useDisclosure, @@ -11,12 +12,20 @@ import { import styles from "./SrsReviewPane.module.scss"; import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import { invoke } from "@tauri-apps/api/tauri"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, ScrollRestoration, useNavigate } from "react-router-dom"; import { ArrowBackIcon } from "@chakra-ui/icons"; import ConfirmQuitModal from "../components/utils/ConfirmQuitModal"; import * as _ from "lodash-es"; import { romajiToKana } from "../lib/kanaHelper"; -import { ReviewItem, ReviewItemType, SrsEntry } from "../types/Srs"; +import { + ReviewItem, + ReviewItemType, + SrsEntry, + SrsQuestionGroup, + allQuestionsAnswered, + groupUpdatedLevel, + isGroupCorrect, +} from "../lib/srs"; import classNames from "classnames"; const batchSize = 10; @@ -37,10 +46,12 @@ function Done() { export function Component() { // null = has not started, (.length == 0) = finished const [reviewQueue, setReviewQueue] = useState(null); + const [completedQueue, setCompletedQueue] = useState([]); + const [anyProgress, setAnyProgress] = useState(false); const [startingSize, setStartingSize] = useState(null); const [currentAnswer, setCurrentAnswer] = useState(""); - const [isIncorrect, setIsIncorrect] = useState(false); + const [incorrectTimes, setIncorrectTimes] = useState(0); const { isOpen, onOpen, onClose } = useDisclosure(); const navigate = useNavigate(); @@ -48,20 +59,36 @@ export function Component() { if (!reviewQueue) { invoke("generate_review_batch") .then((result) => { - const newReviews: ReviewItem[] = result.flatMap((srsEntry) => [ - { - associatedId: srsEntry.id, + const newReviews: ReviewItem[] = result.flatMap((srsEntry) => { + // L @ breaking type safety, but this is mutually recursive too + const srsQuestionGroup: SrsQuestionGroup = { + srsEntry, + questions: {}, + } as SrsQuestionGroup; + + const meaningQuestion: ReviewItem = { + parent: srsQuestionGroup, type: ReviewItemType.MEANING, challenge: srsEntry.associated_kanji, possibleAnswers: srsEntry.meanings, - }, - { - associatedId: srsEntry.id, + isCorrect: null, + timesRepeated: 0, + }; + + const readingQuestion: ReviewItem = { + parent: srsQuestionGroup, type: ReviewItemType.READING, challenge: srsEntry.associated_kanji, possibleAnswers: srsEntry.readings, - }, - ]); + isCorrect: null, + timesRepeated: 0, + }; + + srsQuestionGroup.questions.meaningQuestion = meaningQuestion; + srsQuestionGroup.questions.readingQuestion = readingQuestion; + + return [meaningQuestion, readingQuestion]; + }); const newReviewsShuffled = _.shuffle(newReviews); setReviewQueue(newReviewsShuffled); @@ -75,7 +102,7 @@ export function Component() { if (!reviewQueue) return ; if (reviewQueue.length == 0) return ; - const nextItem = reviewQueue[0]; + const [nextItem, ...restOfQueue] = reviewQueue; const possibleAnswers = new Set(nextItem.possibleAnswers); const formSubmit = async (evt: FormEvent) => { @@ -83,28 +110,50 @@ export function Component() { if (!reviewQueue) return; const isCorrect = possibleAnswers.has(currentAnswer); + nextItem.isCorrect = + new Map([ + [null, isCorrect], + [false, false], + [true, isCorrect], + ]).get(nextItem.isCorrect) ?? isCorrect; - // Update the backend - const params = { itemId: nextItem.associatedId, correct: isCorrect }; - const result = await invoke("update_srs_item", params); - console.log("result", result); + // Figure out if we need to update the backend + if (allQuestionsAnswered(nextItem.parent)) { + console.log("SHIET"); - // Check the answer + const group = nextItem.parent; + const newLevel = groupUpdatedLevel(group); + + const params = { + itemId: nextItem.parent.srsEntry.id, + correct: isGroupCorrect(nextItem.parent), + newGrade: newLevel.value, + delay: newLevel.delay, + }; + + const result = await invoke("update_srs_item", params); + console.log("result", result); + } + + // If it's wrong this time if (!isCorrect) { - setIsIncorrect(true); - - // push it to the back of the queue - const lastItem = reviewQueue[reviewQueue.length - 1]; - if (!_.isEqual(lastItem, nextItem)) setReviewQueue([...reviewQueue, nextItem]); + setCurrentAnswer(""); + setIncorrectTimes(incorrectTimes + 1); return; } // Set up for next question! setAnyProgress(true); - setIsIncorrect(false); + setIncorrectTimes(0); setCurrentAnswer(""); - const [_currentItem, ...rest] = reviewQueue; - setReviewQueue(rest); + + if (nextItem.isCorrect || nextItem.timesRepeated > 0) { + setCompletedQueue([...completedQueue, nextItem]); + setReviewQueue(restOfQueue); + } else { + nextItem.timesRepeated++; + setReviewQueue([...restOfQueue, nextItem]); + } }; const inputBox = (kanaInput: boolean) => { @@ -115,8 +164,12 @@ export function Component() { setCurrentAnswer(newValue); }; - const className = classNames(styles["input-box"], isIncorrect && styles["incorrect"]); - const placeholder = isIncorrect ? "Wrong! Try again..." : "Enter your answer..."; + const className = classNames(styles["input-box"], incorrectTimes > 0 && styles["incorrect"]); + const placeholder = + { + 0: "Enter your answer...", + 1: "Wrong! Try again...", + }[incorrectTimes] || `Answer is: ${nextItem.possibleAnswers.join(", ")}`; return ( @@ -140,13 +193,16 @@ export function Component() { return ( <> +

{JSON.stringify(completedQueue.map((x) => x.challenge))}

+

{JSON.stringify(reviewQueue.map((x) => x.challenge))}

+ {startingSize && ( )} @@ -182,11 +238,6 @@ export function Component() { Back -
- Debug -
{JSON.stringify(nextItem, null, 2)}
-
-
{renderInside()}
diff --git a/src/types/Srs.ts b/src/types/Srs.ts deleted file mode 100644 index cee3998..0000000 --- a/src/types/Srs.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface SrsEntry { - id: number; - associated_kanji: string; - current_grade: number; - meanings: string[]; - readings: string[]; -} - -export enum ReviewItemType { - MEANING = "MEANING", - READING = "READING", -} - -export interface ReviewItem { - associatedId: number; - type: ReviewItemType; - challenge: string; - possibleAnswers: string[]; -}