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 { /** Short notebook title (not the raw user prompt). */ notebookName?: string notes: GeneratedNote[] schemaProperties: Array<{ name: string; type: string; options?: string[] }> } export type WizardProfile = 'student' | 'teacher' | 'engineer' /** Word-count targets by level — Expert was failing (timeouts / truncated JSON). */ function wordTargetsForLevel(level?: string): { min: number; max: number; label: string } { const l = (level || '').toLowerCase() if (/expert|avancé|advanced|fort/.test(l)) { return { min: 350, max: 700, label: 'expert (350–700 mots/note — dense mais JSON-fiable)' } } if (/intermédiaire|intermediate|moyen/.test(l)) { return { min: 250, max: 500, label: 'intermédiaire (250–500 mots/note)' } } if (/débutant|beginner|facile|easy/.test(l)) { return { min: 150, max: 350, label: 'débutant (150–350 mots/note)' } } return { min: 200, max: 450, label: 'standard (200–450 mots/note)' } } export class NotebookWizardService { async generateCarnet( profile: WizardProfile, topic: string, options?: { level?: string; count?: number; language?: string } ): Promise { const lang = options?.language || 'fr' // Cap count in expert mode to avoid oversized responses const level = options?.level const isExpert = /expert/i.test(level || '') const count = Math.min(options?.count || 6, isExpert ? 5 : 10) const prompts = this.buildPrompt(profile, topic, lang, count, level) const config = await getSystemConfig() const provider = getChatProvider(config) let raw: string try { raw = await provider.generateText(prompts) } catch (err) { // Retry once with lighter prompt if expert/full generation fails if (isExpert) { const light = this.buildPrompt(profile, topic, lang, Math.min(count, 4), 'Avancé') raw = await provider.generateText(light) } else { throw err } } const result = this.parseResponse(raw, profile, lang) if (!result.notebookName?.trim()) { result.notebookName = this.deriveNotebookName(topic, result.notes, lang) } return result } /** Fallback short name when the model omits notebookName. */ private deriveNotebookName(topic: string, notes: GeneratedNote[], lang: string): string { const fromNote = notes[0]?.title?.trim() if (fromNote && fromNote.length <= 80 && fromNote.length >= 3) { // Prefer first chapter title cleaned of numbering return fromNote.replace(/^\d+[\.\):\-–]\s*/, '').slice(0, 80) } const cleaned = topic .replace(/\s+/g, ' ') .trim() .replace(/^(je veux|i want|please|peux-tu|can you|crée|create|génère|generate)\b[\s,:-]*/i, '') if (cleaned.length > 0 && cleaned.length <= 60) return cleaned if (cleaned.length > 60) { const cut = cleaned.slice(0, 57) const lastSpace = cut.lastIndexOf(' ') return (lastSpace > 20 ? cut.slice(0, lastSpace) : cut) + '…' } return lang === 'fr' ? 'Nouveau carnet' : 'New notebook' } 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 === 'ar' ? 'العربية' : lang === 'de' ? 'Deutsch' : lang === 'es' ? 'Español' : lang === 'it' ? 'Italiano' : lang === 'pt' ? 'Português' : lang === 'ru' ? 'Русский' : lang === 'zh' ? '中文' : lang === 'ja' ? '日本語' : lang === 'ko' ? '한국어' : lang === 'nl' ? 'Nederlands' : lang === 'pl' ? 'Polski' : lang === 'hi' ? 'हिन्दी' : lang const words = wordTargetsForLevel(level) const schemaLabels = this.getSchemaLabels(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 (intention de l'utilisateur, NE PAS recopier tel quel comme titre de carnet) : "${topic}" ${level ? `Niveau : ${level}` : ''} Langue : ${langName} Nombre de notes à créer : ${count} Profondeur cible : ${words.label} CRÉE ${count} NOTES COMPLÈTES sur ce sujet. Chaque note : environ ${words.min}–${words.max} mots (qualité > longueur extrême). IMPORTANT — NOM DU CARNET : - Fournis "notebookName" : un titre COURT et PERTINENT (3–8 mots, max 60 caractères). - Exemple : pour un long message sur la thermo HVAC → "Modélisation thermodynamique HVAC" - N'utilise JAMAIS la phrase complète de l'utilisateur comme nom. Le contenu doit être : - Clair et structuré avec

et

- Des exemples concrets - Des définitions dans des callouts ${profile === 'student' ? '- Points clés en callout tip\n- Pièges à éviter en callout danger' : ''} ${profile === 'teacher' ? '- Objectifs pédagogiques\n- Section Exercices (3–5 questions)' : ''} ${profile === 'engineer' ? '- Spécifications techniques\n- Tableaux comparatifs si utile' : ''} FORMAT DE SORTIE — JSON UNIQUEMENT (pas de markdown hors du bloc) : \`\`\`json { "notebookName": "Titre court du carnet", "notes": [ { "title": "Titre de chapitre descriptif", "difficulty": "facile", "content": "

Introduction

...

..." } ], "schemaProperties": [ { "name": "${schemaLabels.status}", "type": "select", "options": ["${schemaLabels.statusTodo}", "${schemaLabels.statusInProgress}", "${schemaLabels.statusDone}"] }, { "name": "${schemaLabels.difficulty}", "type": "select", "options": ["${schemaLabels.easy}", "${schemaLabels.medium}", "${schemaLabels.hard}"] } ] } \`\`\` BLOCS HTML DISPONIBLES : 1. Callout :

...

(info|warning|tip|success|danger) 2. Toggle :

Titre

Contenu

3. Math :
— JAMAIS $$ 4. Colonnes :
...
5. Sommaire :
6. HTML :

/

,

,