feat(flashcards): révision SM-2, génération IA et page /revision
Livre US-FLASHCARDS avec decks, session de révision, stats et migration Prisma. Finalise le Web Clipper (i18n 15 langues) et corrige les erreurs ESLint bloquant la CI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
90
memento-note/lib/flashcards/deck-queries.ts
Normal file
90
memento-note/lib/flashcards/deck-queries.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { isCardMastered } from '@/lib/flashcards/sm2'
|
||||
|
||||
export interface DeckSummary {
|
||||
id: string
|
||||
name: string
|
||||
notebookId: string | null
|
||||
totalCards: number
|
||||
dueCount: number
|
||||
masteredCount: number
|
||||
lastReviewedAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export async function listDeckSummaries(userId: string): Promise<DeckSummary[]> {
|
||||
const now = new Date()
|
||||
const decks = await prisma.flashcardDeck.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
flashcards: {
|
||||
select: {
|
||||
id: true,
|
||||
interval: true,
|
||||
nextReviewAt: true,
|
||||
reviews: {
|
||||
orderBy: { reviewedAt: 'desc' },
|
||||
take: 1,
|
||||
select: { reviewedAt: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
})
|
||||
|
||||
return decks.map((deck) => {
|
||||
const totalCards = deck.flashcards.length
|
||||
const dueCount = deck.flashcards.filter((c) => c.nextReviewAt <= now).length
|
||||
const masteredCount = deck.flashcards.filter((c) => isCardMastered(c.interval)).length
|
||||
const lastReview = deck.flashcards
|
||||
.flatMap((c) => c.reviews.map((r) => r.reviewedAt))
|
||||
.sort((a, b) => b.getTime() - a.getTime())[0]
|
||||
|
||||
return {
|
||||
id: deck.id,
|
||||
name: deck.name,
|
||||
notebookId: deck.notebookId,
|
||||
totalCards,
|
||||
dueCount,
|
||||
masteredCount,
|
||||
lastReviewedAt: lastReview ? lastReview.toISOString() : null,
|
||||
createdAt: deck.createdAt.toISOString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getDeckDetail(userId: string, deckId: string) {
|
||||
const deck = await prisma.flashcardDeck.findFirst({
|
||||
where: { id: deckId, userId },
|
||||
include: {
|
||||
flashcards: {
|
||||
orderBy: { nextReviewAt: 'asc' },
|
||||
include: {
|
||||
note: { select: { id: true, title: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!deck) return null
|
||||
|
||||
const now = new Date()
|
||||
return {
|
||||
id: deck.id,
|
||||
name: deck.name,
|
||||
notebookId: deck.notebookId,
|
||||
cards: deck.flashcards.map((c) => ({
|
||||
id: c.id,
|
||||
front: c.front,
|
||||
back: c.back,
|
||||
type: c.type,
|
||||
interval: c.interval,
|
||||
easinessFactor: c.easinessFactor,
|
||||
nextReviewAt: c.nextReviewAt.toISOString(),
|
||||
noteId: c.noteId,
|
||||
noteTitle: c.note?.title ?? null,
|
||||
due: c.nextReviewAt <= now,
|
||||
mastered: isCardMastered(c.interval),
|
||||
})),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user