feat: générateur d'exercices + planning de révision IA
- Générateur d'exercices : bouton dans menu note → IA crée 5 exercices - Niveaux variés (facile/moyen/difficile) avec emojis 🟢🟡🔴 - Corrigés détaillés dans des toggles (cliquer pour révéler) - Callout warning pour le niveau - Notes créées dans le même carnet - Planning de révision : bouton dans barre carnet → IA crée planning - Choix date d'examen - Répétition espacée (première lecture → revoir → révision globale) - Rappels automatiques ajoutés aux notes (9h le jour J) - Vue chronologique avec activités et notes par jour - Services : exercise-generator.service.ts + study-planner.service.ts - Endpoints : /api/ai/generate-exercises + /api/ai/study-plan - i18n FR/EN complet
This commit is contained in:
97
memento-note/lib/ai/services/exercise-generator.service.ts
Normal file
97
memento-note/lib/ai/services/exercise-generator.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { getChatProvider } from '../factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export interface GeneratedExercise {
|
||||
question: string
|
||||
difficulty: 'facile' | 'moyen' | 'difficile'
|
||||
answer: string
|
||||
}
|
||||
|
||||
export class ExerciseGeneratorService {
|
||||
async generate(
|
||||
noteTitle: string,
|
||||
noteContent: string,
|
||||
options: { count?: number; language?: string }
|
||||
): Promise<GeneratedExercise[]> {
|
||||
const count = options.count || 5
|
||||
const lang = options.language || 'fr'
|
||||
const langName = lang === 'fr' ? 'français' : lang === 'fa' ? 'فارسی' : 'English'
|
||||
|
||||
// Strip HTML to plain text for the AI
|
||||
const plainText = noteContent
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 4000)
|
||||
|
||||
const prompt = `Tu es un professeur expert. Génère ${count} exercices basés sur ce cours.
|
||||
|
||||
COURS : "${noteTitle}"
|
||||
CONTENU :
|
||||
${plainText}
|
||||
|
||||
Langue : ${langName}
|
||||
|
||||
Génère ${count} exercices variés avec des niveaux de difficulté différents.
|
||||
Pour chaque exercice : une question claire et détaillée, et un corrigé complet et expliqué.
|
||||
|
||||
FORMAT JSON UNIQUEMENT :
|
||||
\`\`\`json
|
||||
[
|
||||
${Array.from({ length: count }, (_, i) => `{
|
||||
"question": "Question détaillée numéro ${i + 1}...",
|
||||
"difficulty": "${['facile', 'moyen', 'difficile'][i % 3]}",
|
||||
"answer": "Corrigé complet et expliqué..."
|
||||
}`).join(',\n ')}
|
||||
]
|
||||
\`\`\`
|
||||
|
||||
RÈGLES :
|
||||
- Les questions doivent couvrir différents aspects du cours
|
||||
- Mélange les types : application directe, analyse, synthèse
|
||||
- Les corrigés doivent être détaillés avec les étapes
|
||||
- Utilise des formules LaTeX avec la notation standard (\\frac, \\sum, etc.)`
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
const raw = await provider.generateText(prompt)
|
||||
|
||||
return this.parseResponse(raw)
|
||||
}
|
||||
|
||||
private parseResponse(raw: string): GeneratedExercise[] {
|
||||
const jsonMatch = raw.match(/```json\s*([\s\S]+?)\s*```/)
|
||||
let jsonStr = jsonMatch ? jsonMatch[1] : raw
|
||||
|
||||
const start = jsonStr.indexOf('[')
|
||||
const end = jsonStr.lastIndexOf(']')
|
||||
if (start >= 0 && end > start) {
|
||||
jsonStr = jsonStr.slice(start, end + 1)
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
return parsed.map((e: any) => ({
|
||||
question: String(e.question || ''),
|
||||
difficulty: e.difficulty || 'moyen',
|
||||
answer: String(e.answer || ''),
|
||||
}))
|
||||
} catch {
|
||||
// Fix backslash escaping
|
||||
try {
|
||||
const fixed = jsonStr.replace(/\\(?!["\\\/bfnrtu])/g, '\\\\')
|
||||
const parsed = JSON.parse(fixed)
|
||||
return parsed.map((e: any) => ({
|
||||
question: String(e.question || ''),
|
||||
difficulty: e.difficulty || 'moyen',
|
||||
answer: String(e.answer || ''),
|
||||
}))
|
||||
} catch {
|
||||
throw new Error('Failed to parse exercises')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const exerciseGeneratorService = new ExerciseGeneratorService()
|
||||
125
memento-note/lib/ai/services/study-planner.service.ts
Normal file
125
memento-note/lib/ai/services/study-planner.service.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { getChatProvider } from '../factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export interface StudyDay {
|
||||
date: string
|
||||
noteIds: string[]
|
||||
noteTitles: string[]
|
||||
activity: string
|
||||
}
|
||||
|
||||
export interface StudyPlan {
|
||||
days: StudyDay[]
|
||||
totalDays: number
|
||||
notesPerDay: number
|
||||
}
|
||||
|
||||
export class StudyPlannerService {
|
||||
async generate(
|
||||
notes: Array<{ id: string; title: string }>,
|
||||
examDate: string,
|
||||
language?: string
|
||||
): Promise<StudyPlan> {
|
||||
const lang = language || 'fr'
|
||||
const langName = lang === 'fr' ? 'français' : lang === 'fa' ? 'فارسی' : 'English'
|
||||
|
||||
const exam = new Date(examDate)
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const daysUntilExam = Math.max(1, Math.ceil((exam.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)))
|
||||
|
||||
const notesList = notes.map((n, i) => `${i + 1}. ${n.title} (id: ${n.id})`).join('\n')
|
||||
|
||||
const prompt = `Tu es un expert en pédagogie et apprentissage. Crée un planning de révision optimisé.
|
||||
|
||||
DATE DE L'EXAMEN : ${examDate}
|
||||
JOURS RESTANTS : ${daysUntilExam}
|
||||
LANGUE : ${langName}
|
||||
|
||||
NOTES À RÉVISER :
|
||||
${notesList}
|
||||
|
||||
Crée un planning qui répartit ces notes sur ${daysUntilExam} jours en utilisant la répétition espacée :
|
||||
- Les premiers jours : couvrir tout le programme une fois
|
||||
- Les jours suivants : revoir les notes difficiles plus fréquemment
|
||||
- Les derniers jours : révision globale et fiches synthèses
|
||||
- Inclus des jours de repos légers (relecture rapide)
|
||||
|
||||
FORMAT JSON UNIQUEMENT :
|
||||
\`\`\`json
|
||||
{
|
||||
"days": [
|
||||
{
|
||||
"date": "YYYY-MM-DD",
|
||||
"noteIds": ["id1", "id2"],
|
||||
"activity": "Description courte de l'activité du jour"
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Génère exactement ${Math.min(daysUntilExam, 30)} entrées de jours (max 30).
|
||||
Les dates vont de aujourd'hui (${today.toISOString().slice(0, 10)}) jusqu'à la veille de l'examen.
|
||||
Chaque jour doit avoir 1-4 notes à réviser avec une activité descriptive.`
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
const raw = await provider.generateText(prompt)
|
||||
|
||||
return this.parseResponse(raw, notes, daysUntilExam)
|
||||
}
|
||||
|
||||
private parseResponse(
|
||||
raw: string,
|
||||
notes: Array<{ id: string; title: string }>,
|
||||
totalDays: number
|
||||
): StudyPlan {
|
||||
const jsonMatch = raw.match(/```json\s*([\s\S]+?)\s*```/)
|
||||
let jsonStr = jsonMatch ? jsonMatch[1] : raw
|
||||
|
||||
const start = jsonStr.indexOf('{')
|
||||
const end = jsonStr.lastIndexOf('}')
|
||||
if (start >= 0 && end > start) {
|
||||
jsonStr = jsonStr.slice(start, end + 1)
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const titleMap = new Map(notes.map(n => [n.id, n.title]))
|
||||
|
||||
const days: StudyDay[] = (parsed.days || []).map((d: any) => ({
|
||||
date: String(d.date || ''),
|
||||
noteIds: Array.isArray(d.noteIds) ? d.noteIds.map(String) : [],
|
||||
noteTitles: (Array.isArray(d.noteIds) ? d.noteIds : []).map((id: string) => titleMap.get(id) || 'Note'),
|
||||
activity: String(d.activity || 'Révision'),
|
||||
}))
|
||||
|
||||
return {
|
||||
days,
|
||||
totalDays: days.length,
|
||||
notesPerDay: days.length > 0 ? Math.round(notes.length / days.length) : 0,
|
||||
}
|
||||
} catch {
|
||||
// Fallback: simple distribution
|
||||
const days: StudyDay[] = []
|
||||
const today = new Date()
|
||||
const notesPerDay = Math.max(1, Math.ceil(notes.length / Math.min(totalDays, 14)))
|
||||
for (let i = 0; i < Math.min(totalDays, 14); i++) {
|
||||
const date = new Date(today)
|
||||
date.setDate(date.getDate() + i)
|
||||
const dayNotes = notes.slice(i * notesPerDay, (i + 1) * notesPerDay)
|
||||
if (dayNotes.length === 0 && i > 0) break
|
||||
days.push({
|
||||
date: date.toISOString().slice(0, 10),
|
||||
noteIds: dayNotes.map(n => n.id),
|
||||
noteTitles: dayNotes.map(n => n.title),
|
||||
activity: i === 0 ? 'Première lecture' : `Revoir ${dayNotes.length} notes`,
|
||||
})
|
||||
}
|
||||
|
||||
return { days, totalDays: days.length, notesPerDay }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const studyPlannerService = new StudyPlannerService()
|
||||
Reference in New Issue
Block a user