hoàn thành v 1.0.2 bản áp dụng thuật toán FSRS

This commit is contained in:
2026-04-29 09:27:46 +07:00
parent 8459801ca9
commit 7eabb9fed8
6 changed files with 86 additions and 69 deletions
@@ -1104,7 +1104,8 @@ export class PageService {
nextDifficulty = w[4] - w[5] * (quality - 3);
nextState = quality === 1 ? 1 : 2; // If Again, move to Learning, else Review
} else {
const retrievability = Math.pow(0.9, elapsedDays / stability);
// Avoid division by zero if stability is 0
const retrievability = stability > 0 ? Math.pow(0.9, elapsedDays / stability) : 0;
if (quality === 1) {
// Failed
@@ -1123,8 +1124,9 @@ export class PageService {
}
}
// Constraints
nextStability = Math.min(Math.max(nextStability, 0.1), 36500);
// Constraints & NaN guard
nextStability = Math.min(Math.max(nextStability || 0.1, 0.1), 36500);
nextDifficulty = Math.min(Math.max(nextDifficulty || 5, 1), 10);
// FSRS interval formula: I = S * ln(target_recall) / ln(0.9)
// For default target_recall = 0.9, I = S
@@ -1147,54 +1149,63 @@ export class PageService {
pageId,
);
return this.pageRepo.findById(pageId);
}
async getPagesToReview(userId: string, pagination: PaginationOptions, spaceId?: string, onlyDue = true) {
let query = this.db
.selectFrom('pages')
.innerJoin('spaces', 'spaces.id', 'pages.spaceId')
.select([
'pages.id',
'pages.slugId',
'pages.title',
'pages.icon',
'pages.spaceId',
'pages.nextReviewDate',
'pages.easeFactor',
'pages.repetitionCount',
'pages.reviewInterval',
'pages.createdAt',
'pages.updatedAt',
])
.select(
sql<{ name: string; slug: string }>`json_build_object('name', spaces.name, 'slug', spaces.slug)`.as('space'),
)
.where('pages.nextReviewDate', 'is not', null)
.where('pages.deletedAt', 'is', null)
.orderBy('pages.nextReviewDate', 'asc');
if (onlyDue) {
query = query.where('pages.nextReviewDate', '<=', new Date());
}
if (spaceId) {
query = query.where('pages.spaceId', '=', spaceId);
} else {
query = query.where('pages.spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId));
}
return executeWithCursorPagination(query, {
perPage: pagination.limit,
cursor: pagination.cursor,
fields: [
{ expression: 'pages.nextReviewDate', direction: 'asc' },
{ expression: 'pages.id', direction: 'asc' },
],
parseCursor: (cursor: any) => ({
nextReviewDate: new Date(cursor.nextReviewDate),
id: cursor.id,
}),
console.log('FSRS CALCULATION:', {
pageId,
quality,
nextStability,
nextDifficulty,
nextReviewDate: nextReviewDate.toISOString(),
reviewInterval: Math.round(exactReviewInterval)
});
return this.pageRepo.findById(pageId);
}
async getPagesToReview(userId: string, pagination: PaginationOptions, spaceId ?: string, onlyDue = true) {
let query = this.db
.selectFrom('pages')
.innerJoin('spaces', 'spaces.id', 'pages.spaceId')
.select([
'pages.id',
'pages.slugId',
'pages.title',
'pages.icon',
'pages.spaceId',
'pages.nextReviewDate',
'pages.easeFactor',
'pages.repetitionCount',
'pages.reviewInterval',
'pages.createdAt',
'pages.updatedAt',
])
.select(
sql<{ name: string; slug: string }>`json_build_object('name', spaces.name, 'slug', spaces.slug)`.as('space'),
)
.where('pages.nextReviewDate', 'is not', null)
.where('pages.deletedAt', 'is', null)
.orderBy('pages.nextReviewDate', 'asc');
if (onlyDue) {
query = query.where('pages.nextReviewDate', '<=', new Date());
}
if (spaceId) {
query = query.where('pages.spaceId', '=', spaceId);
} else {
query = query.where('pages.spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId));
}
return executeWithCursorPagination(query, {
perPage: pagination.limit,
cursor: pagination.cursor,
fields: [
{ expression: 'pages.nextReviewDate', direction: 'asc' },
{ expression: 'pages.id', direction: 'asc' },
],
parseCursor: (cursor: any) => ({
nextReviewDate: new Date(cursor.nextReviewDate),
id: cursor.id,
}),
});
}
}