feat: Implement SM2
This commit is contained in:
@@ -18,7 +18,9 @@
|
||||
lastLevel: number,
|
||||
levels: number[],
|
||||
|
||||
queue: number[],
|
||||
vocabMetadata: {
|
||||
[id: number]: ISM2Metadata,
|
||||
},
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -32,12 +32,19 @@ levelRouter.get("/:id/vocab", async (req: LRequest, res: Response) => {
|
||||
}
|
||||
|
||||
// Find the level
|
||||
// TODO: if (level)
|
||||
const { db } = req;
|
||||
const level = await db.collection("levels").findOne({
|
||||
// TODO: This aint safe, boi
|
||||
level: levelId,
|
||||
});
|
||||
if (!level) {
|
||||
res.send({
|
||||
error: "404",
|
||||
data: {
|
||||
msg: `Cannot find level with id "${levelId}"`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all the vocabulary
|
||||
const vocab = await db.collection("vocabulary").find({ id: { $in: level.vocab } }).toArray();
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as express from "express";
|
||||
import * as bodyparser from "body-parser";
|
||||
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { authRoute } from "../security/token";
|
||||
|
||||
import { userFromSession } from "../utils/user";
|
||||
|
||||
import { IUser, IUserDBModel } from "../models/user";
|
||||
import { ISM2Metadata } from "../models/review";
|
||||
import { LRequest } from "../types/express";
|
||||
import { Db } from "mongodb";
|
||||
|
||||
const userRouter = express.Router();
|
||||
userRouter.use(authRoute);
|
||||
@@ -65,13 +67,30 @@ export enum VocabType {
|
||||
userRouter.get("/queue", async (req: LRequest, res) => {
|
||||
// TODO: if (user)
|
||||
const { token, db } = req;
|
||||
const user = await userFromSession(token, db);
|
||||
let user = Object.assign({}, await userFromSession(token, db));
|
||||
|
||||
const sm2 = user.vocabMetadata;
|
||||
const data = Object.keys(sm2).map(id => Object.assign({}, sm2[parseInt(id)], { id, }));
|
||||
const sorted = data.sort((a: any, b: any) => {
|
||||
if (a.nextDueDate > b.nextDueDate)
|
||||
return 1;
|
||||
if (a.nextDueDate < b.nextDueDate)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}).slice(0, 20)
|
||||
.map((el: any) => {
|
||||
return parseInt(el.id);
|
||||
});
|
||||
|
||||
// Fetch all vocab ids from the vocabulary collection
|
||||
const vocabRaw = await db.collection("vocabulary").find({
|
||||
id: { $in: user.queue },
|
||||
id: { $in: sorted },
|
||||
}, {
|
||||
// TODO: Make this configurable?
|
||||
sort: {
|
||||
|
||||
},
|
||||
limit: 20,
|
||||
}).toArray();
|
||||
|
||||
@@ -102,18 +121,69 @@ userRouter.get("/lastReview", async (req: LRequest, res) => {
|
||||
data: user.lastReview,
|
||||
});
|
||||
});
|
||||
|
||||
function dateInNDays(n: number): number {
|
||||
let today = new Date();
|
||||
today.setDate(today.getDate() + n);
|
||||
return Date.parse(today.toString());
|
||||
}
|
||||
|
||||
userRouter.post("/lastReview", async (req: LRequest, res) => {
|
||||
// TODO: Check if we get the correct data
|
||||
// TODO: if(user)
|
||||
// Check if we get the needed data
|
||||
if (!req.body || !("meta" in req.body) || !("sm2" in req.body)) {
|
||||
res.send({
|
||||
error: "400",
|
||||
data: {
|
||||
msg: "Last review state or SM2 data not specified!",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { token, db } = req;
|
||||
const user = await userFromSession(token, db);
|
||||
let user = Object.assign({}, await userFromSession(token, db));
|
||||
if (!user) {
|
||||
res.send({
|
||||
error: "404",
|
||||
data: {
|
||||
msg: "Failed to find user with the specified session!",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(req.body.sm2).forEach((id: string) => {
|
||||
const vocabId = parseInt(id);
|
||||
const correct: boolean = req.body.sm2[id];
|
||||
let vocab_sm2: ISM2Metadata = Object.assign({},
|
||||
user.vocabMetadata[vocabId]);
|
||||
|
||||
// TODO: Tuning?
|
||||
// TODO: Move into another module
|
||||
const perf = correct ? 3 : 1;
|
||||
vocab_sm2.easiness += -0.8 + 0.28 * perf + 0.02 * Math.pow(perf, 2);
|
||||
// Update the consecutive correct answers and the due date
|
||||
if (correct) {
|
||||
vocab_sm2.consecutiveCorrectAnswers += 1;
|
||||
vocab_sm2.nextDueDate = dateInNDays(6 * Math.pow(vocab_sm2.easiness, vocab_sm2.consecutiveCorrectAnswers - 1));
|
||||
} else {
|
||||
vocab_sm2.consecutiveCorrectAnswers = 0;
|
||||
vocab_sm2.nextDueDate = dateInNDays(1);
|
||||
}
|
||||
|
||||
user.vocabMetadata[vocabId] = vocab_sm2;
|
||||
});
|
||||
|
||||
// Update the last review
|
||||
user.lastReview = req.body.meta;
|
||||
|
||||
// TODO: Error handling
|
||||
await db.collection("users").updateOne({
|
||||
username: user.username,
|
||||
}, {
|
||||
$set: {
|
||||
lastReview: req.body.meta,
|
||||
lastReview: user.lastReview,
|
||||
vocabMetadata: user.vocabMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
5
backend/src/models/review.ts
Normal file
5
backend/src/models/review.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ISM2Metadata {
|
||||
easiness: number;
|
||||
consecutiveCorrectAnswers: number;
|
||||
nextDueDate: number;
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ISM2Metadata } from "./review";
|
||||
|
||||
export interface IUserDBModel {
|
||||
username: string;
|
||||
uid: string;
|
||||
@@ -20,12 +22,8 @@ export interface IUserDBModel {
|
||||
queue: number[];
|
||||
|
||||
// Vocabulary ID -> SM2 Metadata
|
||||
metadata: {
|
||||
[key: number]: {
|
||||
easiness: number;
|
||||
nextDueDate: number;
|
||||
consecutiveCorrectAnswers: number;
|
||||
};
|
||||
vocabMetadata: {
|
||||
[key: number]: ISM2Metadata;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user