// « Prochaine meilleure activité » : recommandation justifiée, fondée sur // cartes dues, faiblesses, erreurs à revoir, plan actif et récence d'activité. import { all, get } from "../db/index.ts"; import { masteryForCourse } from "./mastery.ts"; export type Recommendation = { kind: "flashcards" | "quiz" | "error-review" | "exam" | "concept" | "plan" | "chat"; label: string; reason: string; href: string; courseCode: string; priority: number; }; export function recommendations(userId: number, courseCode: string, max = 4): Recommendation[] { const recs: Recommendation[] = []; const due = get<{ n: number }>( `SELECT COUNT(*) as n FROM card_states cs JOIN flashcards f ON f.id = cs.card_id WHERE cs.user_id = ? AND f.course_code = ? AND cs.suspended = 0 AND cs.due_at <= datetime('now')`, userId, courseCode ); if ((due?.n ?? 0) > 0) { recs.push({ kind: "flashcards", label: `Réviser ${due!.n} carte${due!.n > 1 ? "s" : ""} due${due!.n > 1 ? "s" : ""}`, reason: "La répétition espacée ne fonctionne que si les cartes sont revues à temps — ces cartes arrivent à échéance aujourd'hui.", href: `/apprendre/${courseCode.toLowerCase()}/flashcards`, courseCode, priority: 90 + Math.min(9, due!.n), }); } const errors = get<{ n: number }>( "SELECT COUNT(*) as n FROM error_notebook WHERE user_id = ? AND course_code = ? AND status = 'a-revoir'", userId, courseCode ); if ((errors?.n ?? 0) >= 3) { recs.push({ kind: "error-review", label: `Revoir ${errors!.n} erreurs du cahier`, reason: "Retravailler ses propres erreurs est l'une des formes de pratique les plus rentables.", href: `/apprendre/${courseCode.toLowerCase()}/erreurs`, courseCode, priority: 75, }); } const mastery = masteryForCourse(userId, courseCode); const weakest = mastery.filter((c) => c.observations > 0 && c.score < 0.5).sort((a, b) => a.score - b.score)[0]; if (weakest) { recs.push({ kind: "quiz", label: `Quiz ciblé : ${weakest.name}`, reason: `Votre maîtrise estimée de « ${weakest.name} » est de ${(weakest.score * 100).toFixed(0)} % — quelques questions ciblées la feront progresser.`, href: `/apprendre/${courseCode.toLowerCase()}/quiz?concept=${weakest.slug}`, courseCode, priority: 70, }); } const fresh = mastery.filter((c) => c.observations === 0 && c.importance >= 2).sort((a, b) => (a.week ?? 0) - (b.week ?? 0))[0]; if (fresh) { recs.push({ kind: "concept", label: `Découvrir : ${fresh.name}`, reason: `Notion importante de la semaine ${fresh.week ?? "?"} encore jamais pratiquée — mieux vaut la rencontrer avant qu'elle s'accumule.`, href: `/apprendre/${courseCode.toLowerCase()}/concepts?focus=${fresh.slug}`, courseCode, priority: 50, }); } const plan = get<{ id: number; plan: string }>( "SELECT id, plan FROM study_plans WHERE user_id = ? AND course_code = ? AND active = 1 ORDER BY id DESC LIMIT 1", userId, courseCode ); if (plan) { try { const daysArr = JSON.parse(plan.plan) as { date: string; items: { label: string; done?: boolean }[] }[]; const today = new Date().toISOString().slice(0, 10); const todayPlan = daysArr.find((d) => d.date === today); const pending = todayPlan?.items.filter((i) => !i.done) ?? []; if (pending.length) { recs.push({ kind: "plan", label: `Plan du jour : ${pending[0].label}`, reason: `Votre plan d'étude prévoit ${pending.length} activité${pending.length > 1 ? "s" : ""} aujourd'hui.`, href: `/apprendre/${courseCode.toLowerCase()}/plan`, courseCode, priority: 85, }); } } catch { /* plan illisible — ignoré */ } } const examTried = get<{ n: number }>( `SELECT COUNT(*) as n FROM exam_attempts ea JOIN mock_exams me ON me.id = ea.exam_id WHERE ea.user_id = ? AND me.course_code = ?`, userId, courseCode ); const avgScore = mastery.filter((m) => m.observations > 0); const avg = avgScore.length ? avgScore.reduce((s, m) => s + m.score, 0) / avgScore.length : 0; if ((examTried?.n ?? 0) === 0 && avg >= 0.5) { recs.push({ kind: "exam", label: "Premier examen blanc (mode pratique)", reason: "Votre maîtrise moyenne dépasse 50 % — un examen blanc révélera où concentrer la suite.", href: `/apprendre/${courseCode.toLowerCase()}/examens`, courseCode, priority: 60, }); } const recentChat = all( "SELECT id FROM activity_log WHERE user_id = ? AND course_code = ? AND created_at >= datetime('now','-7 days') LIMIT 1", userId, courseCode ); if (!recentChat.length) { recs.push({ kind: "chat", label: "Poser une question au cours", reason: "Aucune activité cette semaine — reprendre par une question sur la dernière séance est un bon réamorçage.", href: `/chat?course=${courseCode}`, courseCode, priority: 40, }); } return recs.sort((a, b) => b.priority - a.priority).slice(0, max); }