- Menu déroulant GraduationCap : Flashcards + Exercices réunis - Fix: language non défini dans toolbar (useLanguage destructuring) - Fix: équations 658071 → KaTeX dans exercices (preprocessMathInHtml partagé) - lib/text/math-preprocess.ts : utilitaire partagé wizard + exercices - Toast avec bouton 'Voir' pour rafraîchir après création exercices - emitNoteChange pour rafraîchir la liste - i18n FR/EN
177 lines
7.1 KiB
TypeScript
177 lines
7.1 KiB
TypeScript
import { getChatProvider } from '../factory'
|
|
import { getSystemConfig } from '@/lib/config'
|
|
import { preprocessMathInHtml } from '@/lib/text/math-preprocess'
|
|
|
|
export interface GeneratedNote {
|
|
title: string
|
|
content: string
|
|
difficulty?: 'facile' | 'moyen' | 'difficile'
|
|
}
|
|
|
|
export interface GeneratedCarnet {
|
|
notes: GeneratedNote[]
|
|
schemaProperties: Array<{ name: string; type: string; options?: string[] }>
|
|
}
|
|
|
|
export type WizardProfile = 'student' | 'teacher' | 'engineer'
|
|
|
|
export class NotebookWizardService {
|
|
async generateCarnet(
|
|
profile: WizardProfile,
|
|
topic: string,
|
|
options?: { level?: string; count?: number; language?: string }
|
|
): Promise<GeneratedCarnet> {
|
|
const lang = options?.language || 'fr'
|
|
const count = options?.count || 6
|
|
|
|
const prompts = this.buildPrompt(profile, topic, lang, count, options?.level)
|
|
const config = await getSystemConfig()
|
|
const provider = getChatProvider(config)
|
|
const raw = await provider.generateText(prompts)
|
|
|
|
return this.parseResponse(raw, profile)
|
|
}
|
|
|
|
private buildPrompt(
|
|
profile: WizardProfile,
|
|
topic: string,
|
|
lang: string,
|
|
count: number,
|
|
level?: string
|
|
): string {
|
|
const langName = lang === 'fr' ? 'français' : lang === 'en' ? 'English' : lang === 'fa' ? 'فارسی' : lang
|
|
|
|
const profileContext = {
|
|
student: `Tu crées des notes de cours pour un étudiant. Le contenu doit être pédagogique, clair, avec des exemples.`,
|
|
teacher: `Tu crées la structure d'un cours pour un professeur. Chaque note est un chapitre avec des sections à remplir, des objectifs, et des exercices.`,
|
|
engineer: `Tu crées une documentation technique. Le contenu doit être précis, structuré, avec des spécifications et des références.`,
|
|
}[profile]
|
|
|
|
return `Tu es un expert pédagogue et créateur de contenu de haut niveau. ${profileContext}
|
|
|
|
Sujet : "${topic}"
|
|
${level ? `Niveau : ${level}` : ''}
|
|
Langue : ${langName}
|
|
Nombre de notes à créer : ${count}
|
|
|
|
CRÉE ${count} NOTES COMPLÈTES ET DÉTAILLÉES sur ce sujet. Chaque note doit faire ENTRE 800 ET 1500 MOTS minimum. C'est non négociable.
|
|
|
|
Le contenu doit être :
|
|
- COMPLET et APPROFONDI — pas de résumé superficiel
|
|
- Structuré avec des titres <h2> et <h3> pour les sections
|
|
- Des paragraphes développés avec des explications détaillées
|
|
- Des exemples concrets et des cas d'usage
|
|
- Des définitions précises dans des encadrés callout
|
|
${profile === 'student' ? '- Des "Points clés à retenir" en callout tip\n- Des "Pièges à éviter" en callout danger\n- Des exemples d\'application' : ''}
|
|
${profile === 'teacher' ? '- Des objectifs pédagogiques en début de note\n- Des sections "Exercices" avec 5 questions détaillées\n- Des corrigés indicatifs' : ''}
|
|
${profile === 'engineer' ? '- Des spécifications techniques précises\n- Des tableaux comparatifs\n- Des références aux normes et standards' : ''}
|
|
|
|
FORMAT DE SORTIE — JSON UNIQUEMENT :
|
|
\`\`\`json
|
|
{
|
|
"notes": [
|
|
{
|
|
"title": "Titre détaillé et descriptif",
|
|
"difficulty": "facile",
|
|
"content": "<h2>Introduction</h2><p>Plusieurs paragraphes détaillés...</p>..."
|
|
}
|
|
],
|
|
"schemaProperties": [
|
|
{ "name": "Statut", "type": "select", "options": ["À réviser", "En cours", "Maîtrisé"] },
|
|
{ "name": "Difficulté", "type": "select", "options": ["Facile", "Moyen", "Difficile"] }
|
|
]
|
|
}
|
|
\`\`\`
|
|
|
|
BLOCS HTML DISPONIBLES — UTILISE-LES ABONDAMMENT :
|
|
|
|
1. **Callout** (encadré coloré) :
|
|
<div data-type="callout-block" data-callout-type="info"><p>Définition importante...</p></div>
|
|
Types : info, warning, tip, success, danger
|
|
|
|
2. **Toggle** (section repliable pour détails) :
|
|
<div data-type="toggle-block" data-opened="true"><p>Cliquer pour voir les détails</p><p>Contenu détaillé...</p></div>
|
|
|
|
3. **Math** (formules LaTeX) — UTILISE LES BALISES DIRECTEMENT, pas de $$ :
|
|
Block : <div data-type="math-equation" data-latex="E = mc^2"></div>
|
|
Inline : <span data-type="inline-math" data-latex="x^2">x^2</span>
|
|
N'UTILISE JAMAIS $$ ou $ comme délimiteur — utilise TOUJOURS les balises HTML ci-dessus.
|
|
|
|
4. **Colonnes** (comparaison côte à côte) :
|
|
<div data-type="columns" cols="2"><div data-type="column" index="0"><p>Concept A...</p></div><div data-type="column" index="1"><p>Concept B...</p></div></div>
|
|
|
|
5. **Sommaire** (début de note) :
|
|
<div data-type="outline-block"></div>
|
|
|
|
6. HTML standard : <h1>/<h2>/<h3>, <p>, <ul>/<ol>/<li>, <table>, <blockquote>, <pre><code>
|
|
|
|
IMPORTANT : Chaque note DOIT faire entre 800 et 1500 mots. Sois exhaustif. Développe chaque concept avec des exemples, des explications, et du contexte. N'écris pas de résumés courts.
|
|
|
|
Les "difficulty" doivent varier : mélange facile/moyen/difficile.`
|
|
}
|
|
|
|
private parseResponse(raw: string, profile: WizardProfile): GeneratedCarnet {
|
|
const jsonMatch = raw.match(/```json\s*([\s\S]+?)\s*```/)
|
|
let jsonStr = jsonMatch ? jsonMatch[1] : raw
|
|
|
|
// Extract the JSON object boundaries
|
|
const start = jsonStr.indexOf('{')
|
|
const end = jsonStr.lastIndexOf('}')
|
|
if (start >= 0 && end > start) {
|
|
jsonStr = jsonStr.slice(start, end + 1)
|
|
}
|
|
|
|
let parsed: any
|
|
try {
|
|
parsed = JSON.parse(jsonStr)
|
|
} catch {
|
|
// Fix common AI JSON issues: unescaped backslashes in LaTeX
|
|
try {
|
|
const fixed = jsonStr
|
|
.replace(/\\(?!["\\\/bfnrtu])/g, '\\\\') // Escape lone backslashes (LaTeX \frac etc.)
|
|
.replace(/\n/g, '\\n') // Fix raw newlines in strings
|
|
parsed = JSON.parse(fixed)
|
|
} catch {
|
|
try {
|
|
// Last resort: extract notes manually with regex
|
|
const notes: GeneratedNote[] = []
|
|
const titleMatches = [...jsonStr.matchAll(/"title"\s*:\s*"([^"]+)"/g)]
|
|
const contentMatches = [...jsonStr.matchAll(/"content"\s*:\s*"([\s\S]*?)"(?:,|\s*})/g)]
|
|
for (let i = 0; i < titleMatches.length; i++) {
|
|
notes.push({
|
|
title: titleMatches[i][1],
|
|
content: contentMatches[i]?.[1]?.replace(/\\n/g, '\n').replace(/\\"/g, '"') || '<p></p>',
|
|
difficulty: i % 3 === 0 ? 'facile' : i % 3 === 1 ? 'moyen' : 'difficile',
|
|
})
|
|
}
|
|
if (notes.length === 0) throw new Error('No notes found in response')
|
|
return {
|
|
notes,
|
|
schemaProperties: [
|
|
{ name: 'Statut', type: 'select', options: ['À réviser', 'En cours', 'Maîtrisé'] },
|
|
{ name: 'Difficulté', type: 'select', options: ['Facile', 'Moyen', 'Difficile'] },
|
|
],
|
|
}
|
|
} catch {
|
|
throw new Error('Failed to parse AI response')
|
|
}
|
|
}
|
|
}
|
|
|
|
const notes: GeneratedNote[] = (parsed.notes || []).map((n: any) => ({
|
|
title: String(n.title || 'Sans titre'),
|
|
content: preprocessMathInHtml(String(n.content || '<p></p>')),
|
|
difficulty: n.difficulty || undefined,
|
|
}))
|
|
|
|
const schemaProperties = parsed.schemaProperties || [
|
|
{ name: 'Statut', type: 'select', options: ['À réviser', 'En cours', 'Maîtrisé'] },
|
|
{ name: 'Difficulté', type: 'select', options: ['Facile', 'Moyen', 'Difficile'] },
|
|
]
|
|
|
|
return { notes, schemaProperties }
|
|
}
|
|
}
|
|
|
|
export const notebookWizardService = new NotebookWizardService()
|