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%
5.0 KB · 132 lines typescript
Raw Blame History
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