Publication IA: - 4 templates (magazine, brief, essay, simple) avec CSS riche - Rewrite IA (article/exercises/tutorial/reference/mixed) - Modération avec timeout 12s + fallback safe - Quotas publish_enhance par tier (basic=2, pro=15, business=100) - Détection contenu stale (hash) - Migration DB publishedContent/publishedTemplate/publishedSourceHash Fixes: - cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast - _isShared ajouté au type Note (champ virtuel serveur) - callout colors PDF export: extraction fonction pure testable - admin/published: guard note.userId null - Cmd+S fonctionne en mode dialog (pas seulement fullPage) i18n: - 23 clés publish* traduites dans les 15 locales - Extension Web Clipper: 13 locales mise à jour Tests: - callout-colors.test.ts (6 tests) - note-visible-in-view.test.ts (5 tests) - entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs - 199/199 tests passent Tracker: user-stories.md sync avec sprint-status.yaml
130 lines
5.3 KiB
TypeScript
130 lines
5.3 KiB
TypeScript
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, language)
|
|
}
|
|
|
|
private parseResponse(
|
|
raw: string,
|
|
notes: Array<{ id: string; title: string }>,
|
|
totalDays: number,
|
|
language?: string
|
|
): 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 lang = language || 'fr'
|
|
const firstReadLabel = lang === 'fa' ? 'اولین مطالعه' : lang === 'ar' ? 'القراءة الأولى' : lang === 'de' ? 'Erste Lektüre' : lang === 'es' ? 'Primera lectura' : lang === 'it' ? 'Prima lettura' : lang === 'pt' ? 'Primeira leitura' : lang === 'ru' ? 'Первое чтение' : lang === 'zh' ? '第一次阅读' : lang === 'ja' ? '最初の読み' : lang === 'ko' ? '첫 번째 읽기' : lang === 'nl' ? 'Eerste lezing' : lang === 'pl' ? 'Pierwsze czytanie' : lang === 'hi' ? 'पहली पढ़ाई' : lang === 'fr' ? 'Première lecture' : 'First reading'
|
|
const reviewLabel = (n: number) => lang === 'fa' ? `مرور ${n} یادداشت` : lang === 'ar' ? `مراجعة ${n} ملاحظات` : lang === 'de' ? `${n} Notizen wiederholen` : lang === 'es' ? `Repasar ${n} notas` : lang === 'it' ? `Ripassa ${n} note` : lang === 'pt' ? `Rever ${n} notas` : lang === 'ru' ? `Повторить ${n} заметок` : lang === 'zh' ? `复习 ${n} 条笔记` : lang === 'ja' ? `${n}件のノートを復習` : lang === 'ko' ? `${n}개 노트 복습` : lang === 'nl' ? `${n} notities herzien` : lang === 'pl' ? `Powtórz ${n} notatek` : lang === 'hi' ? `${n} नोट्स दोहराएं` : lang === 'fr' ? `Revoir ${n} notes` : `Review ${n} notes`
|
|
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 ? firstReadLabel : reviewLabel(dayNotes.length),
|
|
})
|
|
}
|
|
|
|
return { days, totalDays: days.length, notesPerDay }
|
|
}
|
|
}
|
|
}
|
|
|
|
export const studyPlannerService = new StudyPlannerService()
|