SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
3.2 KB · 66 lines typescript
Raw Blame History
1// Révision d'une carte : SM-2 + événement de maîtrise + journal.2import { NextResponse } from "next/server";3import { z } from "zod";4import { apiError, parseBody } from "@/lib/api.ts";5import { assertSameOrigin, requireUser } from "@/lib/auth/session.ts";6import { get, run } from "@/lib/db/index.ts";7import { initialCardState, reviewCard, type ReviewQuality } from "@/lib/learning/sm2.ts";8import { recordMasteryEvent } from "@/lib/learning/mastery.ts";9import { logActivity } from "@/lib/usage.ts";1011const schema = z.object({12  cardId: z.number().int().positive(),13  q: z.union([z.literal(2), z.literal(3), z.literal(4), z.literal(5)]),14  suspend: z.boolean().optional(),15  favorite: z.boolean().optional(),16});1718export async function POST(req: Request) {19  try {20    await assertSameOrigin();21    const user = await requireUser();22    const body = await parseBody(req, schema);23    const card = get<{ id: number; course_code: string; concept_id: number | null; owner_user_id: number | null }>(24      "SELECT id, course_code, concept_id, owner_user_id FROM flashcards WHERE id = ?", body.cardId25    );26    if (!card || (card.owner_user_id && card.owner_user_id !== user.id)) {27      return NextResponse.json({ error: "Carte introuvable." }, { status: 404 });28    }29    const enrolled = get("SELECT 1 as ok FROM enrollments WHERE user_id = ? AND course_code = ?", user.id, card.course_code);30    if (!enrolled) return NextResponse.json({ error: "Accès refusé." }, { status: 403 });3132    const state = get<{ ef: number; interval_days: number; reps: number; lapses: number }>(33      "SELECT ef, interval_days, reps, lapses FROM card_states WHERE user_id = ? AND card_id = ?", user.id, body.cardId34    );35    const prev = state36      ? { ef: state.ef, intervalDays: state.interval_days, reps: state.reps, lapses: state.lapses }37      : initialCardState();38    const next = reviewCard(prev, body.q as ReviewQuality);39    const dueExpr = next.dueInDays === 0 ? "datetime('now', '+10 minutes')" : `datetime('now', '+${next.dueInDays} days')`;4041    run(42      `INSERT INTO card_states (user_id, card_id, ef, interval_days, reps, lapses, due_at, suspended, favorite, last_reviewed_at)43       VALUES (?, ?, ?, ?, ?, ?, ${dueExpr}, ?, ?, datetime('now'))44       ON CONFLICT(user_id, card_id) DO UPDATE SET ef = excluded.ef, interval_days = excluded.interval_days,45         reps = excluded.reps, lapses = excluded.lapses, due_at = excluded.due_at,46         suspended = excluded.suspended, favorite = excluded.favorite, last_reviewed_at = excluded.last_reviewed_at`,47      user.id, body.cardId, next.ef, next.intervalDays, next.reps, next.lapses,48      body.suspend ? 1 : 0, body.favorite ? 1 : 049    );50    run(51      "INSERT INTO review_log (user_id, card_id, q, interval_before) VALUES (?, ?, ?, ?)",52      user.id, body.cardId, body.q, prev.intervalDays53    );54    if (card.concept_id) {55      recordMasteryEvent({56        userId: user.id, conceptId: card.concept_id, kind: "flashcard",57        correct: body.q >= 4, difficulty: body.q === 3 ? 3 : 2,58      });59    }60    logActivity(user.id, "flashcards", card.course_code, 20);61    return NextResponse.json({ ok: true, nextDueInDays: next.dueInDays, ef: next.ef });62  } catch (e) {63    return apiError(e);64  }65}66