feat: Wizard IA + Export PDF + fixes blocs
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m7s
CI / Deploy production (on server) (push) Has been skipped

- 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:
Antigravity
2026-06-14 19:51:02 +00:00
parent f7b62009cf
commit 940c3daf62
10 changed files with 915 additions and 33 deletions

View File

@@ -19,7 +19,7 @@ import { Badge } from '@/components/ui/badge'
import {
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap, FileDown, FileUp, Mic, MicOff
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap, FileDown, FileUp, Mic, MicOff, Printer
} from 'lucide-react'
import { FlashcardGenerateDialog } from '@/components/flashcards/flashcard-generate-dialog'
import { NoteShareDialog } from './note-share-dialog'
@@ -89,6 +89,124 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
}
}
const handleExportPdf = async () => {
const editor = richTextEditorRef?.current?.getEditor()
if (!editor) return
const title = state.title || note.title || 'Note'
toast.loading(t('richTextEditor.pdfExportLoading') || 'Génération du PDF...', { id: 'pdf-export' })
try {
const editorEl = document.querySelector('.ProseMirror') as HTMLElement
if (!editorEl) throw new Error('Editor not found')
// Clone the editor DOM to process it for print
const clone = editorEl.cloneNode(true) as HTMLElement
// Remove all action buttons, toolbars, and UI elements
clone.querySelectorAll('button, .drag-handle, [contenteditable="false"], .opacity-0, .group-hover\\:opacity-100').forEach(el => el.remove())
clone.querySelectorAll('[title]').forEach(el => {
const title = el.getAttribute('title')
if (title && (title.includes('Supprimer') || title.includes('Delete') || title.includes('Désactiver') || title.includes('Disable') || title.includes('Modifier') || title.includes('Edit'))) {
el.remove()
}
})
// Render KaTeX equations properly
const katex = (await import('katex')).default
clone.querySelectorAll('.math-equation-block').forEach(el => {
const latex = el.getAttribute('data-latex') || el.textContent || ''
try {
el.innerHTML = katex.renderToString(latex, { displayMode: true, throwOnError: false })
} catch {}
})
clone.querySelectorAll('.inline-math').forEach(el => {
const latex = el.getAttribute('data-latex') || el.textContent || ''
try {
el.innerHTML = katex.renderToString(latex, { displayMode: false, throwOnError: false })
} catch {}
})
// Force show all toggle content
clone.querySelectorAll('[class*="hidden"]').forEach(el => {
(el as HTMLElement).style.display = 'block'
})
// Apply callout colors as inline styles (Tailwind classes won't work in print window)
clone.querySelectorAll('[data-callout-type]').forEach(el => {
const type = el.getAttribute('data-callout-type')
const colors: Record<string, { bg: string; border: string }> = {
info: { bg: '#eff6ff', border: '#93c5fd' },
warning: { bg: '#fffbeb', border: '#fcd34d' },
tip: { bg: '#faf5ff', border: '#c4b5fd' },
success: { bg: '#f0fdf4', border: '#86efac' },
danger: { bg: '#fef2f2', border: '#fca5a5' },
}
const c = colors[type || 'info'] || colors.info
const inner = el.querySelector('div')
if (inner) {
(inner as HTMLElement).style.background = c.bg
(inner as HTMLElement).style.borderColor = c.border
}
})
const cloneHtml = clone.innerHTML
const printWindow = window.open('', '_blank')
if (!printWindow) {
toast.error(t('richTextEditor.pdfExportBlocked') || 'Popup bloqué', { id: 'pdf-export' })
return
}
printWindow.document.write(`<!DOCTYPE html><html dir="auto"><head><meta charset="utf-8"><title>${title}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<style>
@page { margin: 1.5cm; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 720px; margin: 0 auto; line-height: 1.7; color: #1a1a1a; font-size: 14px; }
h1 { font-size: 1.8em; border-bottom: 2px solid #e5e5e5; padding-bottom: 0.3em; margin-bottom: 0.5em; }
h2 { font-size: 1.4em; margin-top: 1.5em; }
h3 { font-size: 1.2em; margin-top: 1.2em; }
p { margin: 0.6em 0; }
ul, ol { padding-left: 1.5em; }
li { margin: 0.3em 0; }
blockquote { border-left: 3px solid #d4d4d4; padding-left: 1em; color: #555; font-style: italic; margin: 0.8em 0; }
pre { background: #f5f5f5; padding: 0.8em; border-radius: 6px; overflow-x: auto; font-size: 13px; }
code { font-family: 'SF Mono', Menlo, monospace; }
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
th, td { border: 1px solid #ddd; padding: 0.5em 0.8em; text-align: left; }
th { background: #f5f5f5; font-weight: 600; }
img { max-width: 100%; height: auto; border-radius: 4px; }
.callout-block { padding: 0.8em 1em; border-radius: 8px; border-left: 4px solid; margin: 0.8em 0; }
.callout-block[data-callout-type="info"] { background: #eff6ff; border-color: #3b82f6; }
.callout-block[data-callout-type="warning"] { background: #fffbeb; border-color: #f59e0b; }
.callout-block[data-callout-type="tip"] { background: #faf5ff; border-color: #8b5cf6; }
.callout-block[data-callout-type="success"] { background: #f0fdf4; border-color: #22c55e; }
.callout-block[data-callout-type="danger"] { background: #fef2f2; border-color: #ef4444; }
.toggle-block { border: 1px solid #e5e5e5; border-radius: 8px; margin: 0.8em 0; overflow: hidden; }
.toggle-block > div:first-child { background: #f9f9f9; padding: 0.5em 0.8em; font-weight: 600; font-size: 0.85em; }
.toggle-content { padding: 0.5em 0.8em; }
.columns-block { display: flex; gap: 16px; margin: 0.8em 0; }
.columns-block > div { flex: 1; }
.math-equation-block { margin: 1em 0; text-align: center; }
.outline-block { border: 1px solid #e5e5e5; border-radius: 8px; padding: 0.8em; margin: 1em 0; }
.link-preview-block { border: 1px solid #e5e5e5; border-radius: 8px; overflow: hidden; margin: 0.8em 0; }
.link-preview-searchable { display: none !important; }
a { color: #A47148; text-decoration: none; }
a:hover { text-decoration: underline; }
@media print { body { margin: 0; max-width: 100%; } }
</style></head><body><h1>${title}</h1>${cloneHtml}</body></html>`)
printWindow.document.close()
setTimeout(() => {
printWindow.focus()
printWindow.print()
toast.success(t('richTextEditor.pdfExportSuccess') || 'PDF prêt !', { id: 'pdf-export' })
}, 800)
} catch (e: any) {
toast.error(e.message || 'Erreur export PDF', { id: 'pdf-export' })
}
}
// ── Markdown import ───────────────────────────────────────────────────────
const openMarkdownImport = () => {
const input = document.createElement('input')
@@ -334,6 +452,10 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
<FileDown className="h-4 w-4 me-2" />
{t('richTextEditor.exportMarkdown')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleExportPdf}>
<Printer className="h-4 w-4 me-2" />
{t('richTextEditor.exportPdf') || 'Exporter en PDF'}
</DropdownMenuItem>
<DropdownMenuItem onClick={openMarkdownImport}>
<FileUp className="h-4 w-4 me-2" />
{t('richTextEditor.importMarkdown')}