feat: Wizard IA + Export PDF + fixes blocs
- Wizard IA: création de carnet complet (étudiant/prof/ingénieur)
- 3 profils, choix niveau, nombre de notes (3-12)
- Étapes numérotées, messages de progression, écran de succès
- Notes riches avec callouts, toggles, équations, colonnes
- Structured View auto-créé avec propriétés Statut/Difficulté
- Embeddings en arrière-plan
- Export PDF: clone le DOM réel, rend KaTeX, préserve couleurs callouts
- Fix: boutons toggle/callout en hover-only
- Fix: parsing JSON robuste (backslashes LaTeX)
- Fix: flushSync warning (queueMicrotask)
- Fix: drag handle clamp viewport
- Bouton wizard dans la sidebar (✨ à côté du +)
- i18n FR/EN complet
This commit is contained in:
135
memento-note/app/api/ai/notebook-wizard/route.ts
Normal file
135
memento-note/app/api/ai/notebook-wizard/route.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { notebookWizardService, type WizardProfile } from '@/lib/ai/services/notebook-wizard.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { profile, topic, level, count, language, notebookName, notebookIcon } = await request.json()
|
||||
|
||||
if (!profile || !topic?.trim()) {
|
||||
return NextResponse.json({ error: 'Profile and topic are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
return NextResponse.json(
|
||||
{ error: isTierLocked ? 'feature_locked' : 'quota_exceeded', errorKey: isTierLocked ? 'ai.featureLocked' : 'ai.quotaExceeded', upgradeTier: err.upgradeTier },
|
||||
{ status: 402 },
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const result = await notebookWizardService.generateCarnet(
|
||||
profile as WizardProfile,
|
||||
topic,
|
||||
{ level, count, language: language || 'fr' }
|
||||
)
|
||||
|
||||
// 1. Create notebook
|
||||
const notebook = await prisma.notebook.create({
|
||||
data: {
|
||||
name: notebookName || topic,
|
||||
icon: notebookIcon || '📚',
|
||||
userId: session.user.id,
|
||||
order: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 2. Create schema
|
||||
const schema = await prisma.notebookSchema.create({
|
||||
data: { notebookId: notebook.id },
|
||||
})
|
||||
|
||||
// 3. Create properties
|
||||
const defaultProperties = [
|
||||
{ name: 'Statut', type: 'select', options: ['À réviser', 'En cours', 'Maîtrisé'] },
|
||||
{ name: 'Difficulté', type: 'select', options: ['Facile', 'Moyen', 'Difficile'] },
|
||||
]
|
||||
const propertiesToCreate = result.schemaProperties?.length ? result.schemaProperties : defaultProperties
|
||||
|
||||
const propertyMap = new Map<string, string>()
|
||||
for (let i = 0; i < propertiesToCreate.length; i++) {
|
||||
const p = propertiesToCreate[i]
|
||||
const created = await prisma.notebookProperty.create({
|
||||
data: {
|
||||
schemaId: schema.id,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
options: p.options ? JSON.stringify(p.options) : null,
|
||||
position: i,
|
||||
},
|
||||
})
|
||||
propertyMap.set(p.name, created.id)
|
||||
}
|
||||
|
||||
// 4. Create notes
|
||||
let noteIndex = 0
|
||||
for (const note of result.notes) {
|
||||
const created = await prisma.note.create({
|
||||
data: {
|
||||
title: note.title,
|
||||
content: note.content,
|
||||
userId: session.user.id,
|
||||
notebookId: notebook.id,
|
||||
type: 'richtext',
|
||||
order: noteIndex++,
|
||||
},
|
||||
})
|
||||
|
||||
// 5. Set default property values
|
||||
if (note.difficulty && propertyMap.has('Difficulté')) {
|
||||
const difficultyMap: Record<string, string> = {
|
||||
facile: 'Facile', moyen: 'Moyen', difficile: 'Difficile',
|
||||
easy: 'Facile', medium: 'Moyen', hard: 'Difficile',
|
||||
}
|
||||
const val = difficultyMap[note.difficulty?.toLowerCase() || ''] || 'Moyen'
|
||||
await prisma.noteProperty.create({
|
||||
data: { noteId: created.id, propertyId: propertyMap.get('Difficulté')!, value: val },
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
if (propertyMap.has('Statut')) {
|
||||
await prisma.noteProperty.create({
|
||||
data: { noteId: created.id, propertyId: propertyMap.get('Statut')!, value: 'À réviser' },
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 6. Background embedding
|
||||
void (async () => {
|
||||
try {
|
||||
const { embeddingService } = await import('@/lib/ai/services/embedding.service')
|
||||
const { upsertNoteEmbedding } = await import('@/lib/embeddings')
|
||||
const { chunkIndexingService } = await import('@/lib/ai/services/chunk-indexing.service')
|
||||
const { embedding } = await embeddingService.generateNoteEmbedding(note.title, note.content)
|
||||
await upsertNoteEmbedding(created.id, embedding)
|
||||
await chunkIndexingService.indexNote(created.id, note.title, note.content)
|
||||
} catch (e) {
|
||||
console.error('[Wizard] Background embedding failed:', e)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json({
|
||||
notebookId: notebook.id,
|
||||
notebookName: notebook.name,
|
||||
noteCount: result.notes.length,
|
||||
noteTitles: result.notes.map(n => n.title),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('[Notebook Wizard] Error:', error)
|
||||
return NextResponse.json({ error: error.message || 'Failed to generate notebook' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user