TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// « Prochaine meilleure activité » : recommandation justifiée, fondée sur2// cartes dues, faiblesses, erreurs à revoir, plan actif et récence d'activité.34import { all, get } from "../db/index.ts";5import { masteryForCourse } from "./mastery.ts";67export type Recommendation = {8 kind: "flashcards" | "quiz" | "error-review" | "exam" | "concept" | "plan" | "chat";9 label: string;10 reason: string;11 href: string;12 courseCode: string;13 priority: number;14};1516export function recommendations(userId: number, courseCode: string, max = 4): Recommendation[] {17 const recs: Recommendation[] = [];1819 const due = get<{ n: number }>(20 `SELECT COUNT(*) as n FROM card_states cs JOIN flashcards f ON f.id = cs.card_id21 WHERE cs.user_id = ? AND f.course_code = ? AND cs.suspended = 0 AND cs.due_at <= datetime('now')`,22 userId, courseCode23 );24 if ((due?.n ?? 0) > 0) {25 recs.push({26 kind: "flashcards",27 label: `Réviser ${due!.n} carte${due!.n > 1 ? "s" : ""} due${due!.n > 1 ? "s" : ""}`,28 reason: "La répétition espacée ne fonctionne que si les cartes sont revues à temps — ces cartes arrivent à échéance aujourd'hui.",29 href: `/apprendre/${courseCode.toLowerCase()}/flashcards`,30 courseCode,31 priority: 90 + Math.min(9, due!.n),32 });33 }3435 const errors = get<{ n: number }>(36 "SELECT COUNT(*) as n FROM error_notebook WHERE user_id = ? AND course_code = ? AND status = 'a-revoir'",37 userId, courseCode38 );39 if ((errors?.n ?? 0) >= 3) {40 recs.push({41 kind: "error-review",42 label: `Revoir ${errors!.n} erreurs du cahier`,43 reason: "Retravailler ses propres erreurs est l'une des formes de pratique les plus rentables.",44 href: `/apprendre/${courseCode.toLowerCase()}/erreurs`,45 courseCode,46 priority: 75,47 });48 }4950 const mastery = masteryForCourse(userId, courseCode);51 const weakest = mastery.filter((c) => c.observations > 0 && c.score < 0.5).sort((a, b) => a.score - b.score)[0];52 if (weakest) {53 recs.push({54 kind: "quiz",55 label: `Quiz ciblé : ${weakest.name}`,56 reason: `Votre maîtrise estimée de « ${weakest.name} » est de ${(weakest.score * 100).toFixed(0)} % — quelques questions ciblées la feront progresser.`,57 href: `/apprendre/${courseCode.toLowerCase()}/quiz?concept=${weakest.slug}`,58 courseCode,59 priority: 70,60 });61 }62 const fresh = mastery.filter((c) => c.observations === 0 && c.importance >= 2).sort((a, b) => (a.week ?? 0) - (b.week ?? 0))[0];63 if (fresh) {64 recs.push({65 kind: "concept",66 label: `Découvrir : ${fresh.name}`,67 reason: `Notion importante de la semaine ${fresh.week ?? "?"} encore jamais pratiquée — mieux vaut la rencontrer avant qu'elle s'accumule.`,68 href: `/apprendre/${courseCode.toLowerCase()}/concepts?focus=${fresh.slug}`,69 courseCode,70 priority: 50,71 });72 }7374 const plan = get<{ id: number; plan: string }>(75 "SELECT id, plan FROM study_plans WHERE user_id = ? AND course_code = ? AND active = 1 ORDER BY id DESC LIMIT 1",76 userId, courseCode77 );78 if (plan) {79 try {80 const daysArr = JSON.parse(plan.plan) as { date: string; items: { label: string; done?: boolean }[] }[];81 const today = new Date().toISOString().slice(0, 10);82 const todayPlan = daysArr.find((d) => d.date === today);83 const pending = todayPlan?.items.filter((i) => !i.done) ?? [];84 if (pending.length) {85 recs.push({86 kind: "plan",87 label: `Plan du jour : ${pending[0].label}`,88 reason: `Votre plan d'étude prévoit ${pending.length} activité${pending.length > 1 ? "s" : ""} aujourd'hui.`,89 href: `/apprendre/${courseCode.toLowerCase()}/plan`,90 courseCode,91 priority: 85,92 });93 }94 } catch { /* plan illisible — ignoré */ }95 }9697 const examTried = get<{ n: number }>(98 `SELECT COUNT(*) as n FROM exam_attempts ea JOIN mock_exams me ON me.id = ea.exam_id99 WHERE ea.user_id = ? AND me.course_code = ?`,100 userId, courseCode101 );102 const avgScore = mastery.filter((m) => m.observations > 0);103 const avg = avgScore.length ? avgScore.reduce((s, m) => s + m.score, 0) / avgScore.length : 0;104 if ((examTried?.n ?? 0) === 0 && avg >= 0.5) {105 recs.push({106 kind: "exam",107 label: "Premier examen blanc (mode pratique)",108 reason: "Votre maîtrise moyenne dépasse 50 % — un examen blanc révélera où concentrer la suite.",109 href: `/apprendre/${courseCode.toLowerCase()}/examens`,110 courseCode,111 priority: 60,112 });113 }114115 const recentChat = all(116 "SELECT id FROM activity_log WHERE user_id = ? AND course_code = ? AND created_at >= datetime('now','-7 days') LIMIT 1",117 userId, courseCode118 );119 if (!recentChat.length) {120 recs.push({121 kind: "chat",122 label: "Poser une question au cours",123 reason: "Aucune activité cette semaine — reprendre par une question sur la dernière séance est un bon réamorçage.",124 href: `/chat?course=${courseCode}`,125 courseCode,126 priority: 40,127 });128 }129130 return recs.sort((a, b) => b.priority - a.priority).slice(0, max);131}132