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.0 KB · 67 lines typescript
Raw Blame History
1// Analyse pédagogique : statistiques AGRÉGÉES et anonymisées (seuil de 3 étudiants minimum).2import { NextResponse } from "next/server";3import { apiError } from "@/lib/api.ts";4import { requireRole } from "@/lib/auth/session.ts";5import { all } from "@/lib/db/index.ts";67const MIN_STUDENTS = 3;89export async function GET(req: Request) {10  try {11    await requireRole("instructor");12    const course = new URL(req.url).searchParams.get("course")?.toUpperCase() ?? "IMM1003";1314    const conceptStats = all<{ id: number; name: string; week: number | null; students: number; avg_score: number | null }>(15      `SELECT c.id, c.name, c.week, COUNT(DISTINCT m.user_id) as students, AVG(m.score) as avg_score16       FROM concepts c LEFT JOIN mastery m ON m.concept_id = c.id17       WHERE c.course_code = ? GROUP BY c.id ORDER BY c.week`,18      course19    ).map((r) => ({20      ...r,21      avg_score: r.students >= MIN_STUDENTS ? r.avg_score : null,22      masked: r.students > 0 && r.students < MIN_STUDENTS,23    }));2425    const hardestQuestions = all(26      `SELECT q.id, q.question, q.difficulty, c.name as concept, COUNT(qa.id) as attempts, ROUND(AVG(qa.correct)*100) as success_pct27       FROM quiz_answers qa JOIN quiz_questions q ON q.id = qa.question_id LEFT JOIN concepts c ON c.id = q.concept_id28       WHERE q.course_code = ? GROUP BY q.id HAVING attempts >= ? ORDER BY success_pct LIMIT 15`,29      course, MIN_STUDENTS30    );3132    const commonErrors = all(33      `SELECT c.name as concept, COUNT(*) as n FROM error_notebook e34       LEFT JOIN concepts c ON c.id = e.concept_id35       WHERE e.course_code = ? GROUP BY e.concept_id HAVING COUNT(DISTINCT e.user_id) >= ? ORDER BY n DESC LIMIT 12`,36      course, MIN_STUDENTS37    );3839    const examStats = all(40      `SELECT me.title, COUNT(ea.id) as attempts, ROUND(AVG(ea.score * 100.0 / NULLIF(ea.total,0)),1) as avg_pct41       FROM mock_exams me LEFT JOIN exam_attempts ea ON ea.exam_id = me.id AND ea.finished_at IS NOT NULL42       WHERE me.course_code = ? GROUP BY me.id`,43      course44    ).map((r) => ({ ...r, avg_pct: (r.attempts as number) >= MIN_STUDENTS ? r.avg_pct : null }));4546    const flagged = all(47      `SELECT rf.id, rf.reason, rf.created_at, rf.resolved, m.content as message_preview48       FROM report_flags rf JOIN messages m ON m.id = rf.message_id49       ORDER BY rf.id DESC LIMIT 30`50    ).map((r) => ({ ...r, message_preview: String(r.message_preview).slice(0, 300) }));5152    const feedback = all(53      `SELECT date(m.created_at) as day,54              SUM(CASE WHEN m.feedback = 1 THEN 1 ELSE 0 END) as up,55              SUM(CASE WHEN m.feedback = -1 THEN 1 ELSE 0 END) as down56       FROM messages m JOIN conversations cv ON cv.id = m.conversation_id57       WHERE m.role='assistant' AND cv.course_code = ? AND m.created_at >= datetime('now','-30 days')58       GROUP BY day ORDER BY day`,59      course60    );6162    return NextResponse.json({ course, minStudents: MIN_STUDENTS, conceptStats, hardestQuestions, commonErrors, examStats, flagged, feedback });63  } catch (e) {64    return apiError(e);65  }66}67