From 882328c28eb48dec4b2f93329641b1dabeccffd0 Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Sun, 26 Oct 2025 02:35:30 +0100 Subject: [PATCH] got kanji with ja and en translation working (not both) and all options at random --- client/package.json | 1 + client/pnpm-lock.yaml | 86 +++++++++ client/src/composables/srs.scoring.ts | 152 ++++++++++++++++ client/src/pages/all.vue | 145 ++++++++++------ client/src/pages/index.vue | 12 +- client/src/pages/kanji.vue | 217 +++++++++++------------ client/src/pages/vocab.vue | 5 +- server/src/api/v1/subject/index.ts | 232 +++++++++++++++++++++++++ server/src/index.ts | 2 + server/src/models/reviewitem.model.ts | 51 ++++++ server/src/services/wanikaniService.ts | 117 ++++++++----- 11 files changed, 799 insertions(+), 221 deletions(-) create mode 100644 client/src/composables/srs.scoring.ts create mode 100644 server/src/api/v1/subject/index.ts create mode 100644 server/src/models/reviewitem.model.ts diff --git a/client/package.json b/client/package.json index 4f39700..667c8f5 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "dependencies": { "@fontsource/roboto": "5.2.7", "@mdi/font": "7.4.47", + "axios": "^1.12.2", "pinia": "^3.0.3", "pug": "^3.0.3", "vue": "^3.5.21", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 3b8eda5..b96530a 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@mdi/font': specifier: 7.4.47 version: 7.4.47 + axios: + specifier: ^1.12.2 + version: 1.12.2 pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) @@ -800,6 +803,12 @@ packages: resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} engines: {node: '>=20.19.0'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} @@ -897,6 +906,10 @@ packages: colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -948,6 +961,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -987,6 +1004,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.10: resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} @@ -1218,6 +1239,19 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1440,6 +1474,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1597,6 +1639,9 @@ packages: promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pug-attrs@3.0.0: resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} @@ -2793,6 +2838,16 @@ snapshots: '@babel/parser': 7.28.4 ast-kit: 2.1.3 + asynckit@0.4.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-walk@3.0.0-canary-5: dependencies: '@babel/types': 7.28.4 @@ -2891,6 +2946,10 @@ snapshots: colorjs.io@0.5.2: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comment-parser@1.4.1: {} concat-map@0.0.1: {} @@ -2930,6 +2989,8 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + detect-libc@1.0.3: optional: true @@ -2957,6 +3018,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.25.10: optionalDependencies: '@esbuild/aix-ppc64': 0.25.10 @@ -3274,6 +3342,16 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true @@ -3465,6 +3543,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -3613,6 +3697,8 @@ snapshots: dependencies: asap: 2.0.6 + proxy-from-env@1.1.0: {} + pug-attrs@3.0.0: dependencies: constantinople: 4.0.1 diff --git a/client/src/composables/srs.scoring.ts b/client/src/composables/srs.scoring.ts new file mode 100644 index 0000000..01a130b --- /dev/null +++ b/client/src/composables/srs.scoring.ts @@ -0,0 +1,152 @@ +import axios from 'axios' + +type SubjectType = 'kanji' | 'vocab' | 'both' +type LearningDirection = 'jp->en' | 'en->jp' +type WritingMode = 'kana' | 'kanji' + +interface SubjectOptions { + subject: SubjectType + direction?: LearningDirection + writingMode?: WritingMode + mode?: 'meaning' | 'reading' + writingPractice: boolean +} + +interface ReviewItem { + type: 'kanji' | 'vocab' + subject: string + answers: string[] + pronunciation_audios?: string[], + mode: string, +} + +let currentReviewItem: any = {} +let currentSubjectOptions: SubjectOptions + +/** + * Handles logic for when a subject answer is correct. + * + * @param id - The unique identifier of the subject item. + * @param subjectOptions - Configuration describing what kind of subject + * and learning mode is being used (kanji, vocab, etc.). + * @returns void + */ +const correct = async () => ( + await axios.post('/api/v1/review/correct', { + itemId: currentReviewItem.id, + currentSubjectOptions, + }, { + headers: { + 'Content-Type': 'application/json', + }, + }) +) + +/** + * Handles logic for when a subject answer is incorrect + * + * @param id - The unique identifier of the subject item. + * @param subjectOptions - Configuration describing what kind of subject + * and learning mode is being used (kanji, vocab, etc.). + * @returns void + */ +const incorrect = async () => { + await axios.post('/api/v1/review/incorrect', { + itemId: currentReviewItem.id, + currentSubjectOptions, + }, { + headers: { + 'Content-Type': 'application/json', + }, + }) +} + +/** + * Fetches the next subject from the server + * + * @param subjectOptions - Configuration describing what kind of subject + * and learning mode is being used (kanji, vocab, etc.). + * + * @returns + */ +const nextSubject = async (subjectOptions: SubjectOptions): Promise => { + const res = await axios.post('/api/v1/review/', { subjectOptions }, { + headers: { 'Content-Type': 'application/json' }, + }) + + + const item = res.data + if (!item || item.message === 'No reviews due') return null + + const mode: 'writing_practice' | 'jp_en_meaning' | 'jp_en_reading' | 'en_jp_writing' = item._selectedMode + + currentReviewItem = item + currentSubjectOptions = { ...subjectOptions, mode } + + let subjectText = '' + let answers: string[] = [] + let pronunciation_audios: string[] | undefined = item.pronunciation_audios + + if (mode === 'writing_practice') { + subjectText = item.meanings?.filter(m => m.primary).map(m => m.meaning).join(', ') || '' + answers = [item.characters] + } else if (mode === 'jp_en_meaning') { + subjectText = item.characters + answers = item.meanings?.filter(m => m.accepted_answer).map(m => m.meaning) || [] + } else if (mode === 'jp_en_reading') { + subjectText = item.characters + answers = item.readings?.filter(r => r.accepted_answer).map(r => r.reading) || [] + } else if (mode === 'en_jp_writing') { + subjectText = item.meanings?.filter(m => m.primary).map(m => m.meaning).join(', ') || '' + if (subjectOptions.writingMode === 'kanji') { + answers = [item.characters] + } else if (subjectOptions.writingMode === 'kana') { + answers = item.readings?.filter(r => r.accepted_answer).map(r => r.reading) || [] + } + } + + console.log({ + type: item.type, + subject: subjectText, + answers, + pronunciation_audios, + mode, + }) + + return { + type: item.type, + subject: subjectText, + answers, + pronunciation_audios, + mode, + } +} + + +const getStatistics = async () => { + const res = await axios.get('/api/v1/review/stats', { + headers: { + 'Content-Type': 'application/json', + }, + }) + + return res.data +} + +const getSubjectStatistics = async (id: string) => { + const res = await axios.get(`/api/v1/review/stats/${id}`, { + headers: { + 'Content-Type': 'application/json', + }, + }) + + return res.data +} + +export { + correct, + incorrect, + nextSubject, + getStatistics, + getSubjectStatistics, +} diff --git a/client/src/pages/all.vue b/client/src/pages/all.vue index d67203a..859b024 100644 --- a/client/src/pages/all.vue +++ b/client/src/pages/all.vue @@ -1,64 +1,73 @@