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')}

View File

@@ -746,11 +746,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
useEffect(() => {
if (editor && content !== undefined && content !== lastEmittedContent.current) {
editor.commands.setContent(content || '')
lastEmittedContent.current = content || ''
// TipTap #7338 : dir explicite rtl sur listes (pas auto) après chargement HTML
requestAnimationFrame(() => {
applyClipRtlDirection(editor, { sourceUrl })
const html = content || ''
lastEmittedContent.current = html
queueMicrotask(() => {
if (!editor.isDestroyed) {
editor.commands.setContent(html)
requestAnimationFrame(() => {
applyClipRtlDirection(editor, { sourceUrl })
})
}
})
}
if (content !== undefined) {

View File

@@ -49,6 +49,7 @@ import { toast } from 'sonner'
import { motion, AnimatePresence } from 'motion/react'
import { getNoteDisplayTitle } from '@/lib/note-preview'
import { CreateNotebookDialog } from './create-notebook-dialog'
import { AiNotebookWizard } from './wizard/ai-notebook-wizard'
import { NotificationPanel } from './notification-panel'
import {
DropdownMenu,
@@ -423,7 +424,7 @@ function SidebarCarnetItem({
onClick={onCarnetClick}
onDoubleClick={(e) => { e.stopPropagation(); onRename() }}
className={cn(
'flex-1 flex items-center gap-2.5 px-2 py-1.5 rounded-lg transition-all duration-300 group/item cursor-pointer relative',
'flex-1 flex items-center gap-2.5 px-2 py-1.5 rounded-lg transition-all duration-300 group/item cursor-pointer relative overflow-hidden',
isActive ? 'bg-white dark:bg-white/10 shadow-sm border border-border/40' : 'hover:bg-white/40 dark:hover:bg-white/5'
)}
>
@@ -454,7 +455,15 @@ function SidebarCarnetItem({
)}
</div>
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
{/* Compteur de notes (toujours visible, à droite du nom) */}
{notes.length > 0 && (
<span className="text-[9px] font-bold text-concrete/40 px-1.5 border border-border/40 rounded-full group-hover/item:text-concrete transition-colors shrink-0 ms-auto">
{notes.length}
</span>
)}
{/* Boutons d'action en absolu, toujours à droite */}
<div className="absolute end-0 top-0 bottom-0 flex items-center gap-1 pe-1 ps-6 opacity-0 group-hover/item:opacity-100 transition-opacity bg-gradient-to-l from-[var(--color-paper,white)] dark:from-[var(--color-paper,#1a1a1a)] from-60% to-transparent">
{isPinned && (
<span className="text-brand-accent" title={t('notebook.pinnedFrozenTooltip')}>
<Pin size={9} className="opacity-70" />
@@ -481,12 +490,6 @@ function SidebarCarnetItem({
>
<Trash2 size={10} />
</button>
{notes.length > 0 && (
<span className="text-[9px] font-bold text-concrete/40 px-1.5 border border-border/40 rounded-full group-hover/item:text-concrete transition-colors">
{notes.length}
</span>
)}
</div>
</motion.div>
</div>
@@ -586,6 +589,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const { notebooks, trashNotebook, updateNotebookOrderOptimistic, moveNotebookToParent, refreshNotebooks } = useNotebooks()
const { open: openSearch } = useSearchModal()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [showAiWizard, setShowAiWizard] = useState(false)
const [createParentId, setCreateParentId] = useState<string | null>(null)
const [renamingNotebook, setRenamingNotebook] = useState<Notebook | null>(null)
const [renameValue, setRenameValue] = useState('')
@@ -1360,6 +1364,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
>
<Plus size={15} />
</button>
<button
type="button"
onClick={() => setShowAiWizard(true)}
className="p-1 hover:bg-brand-accent/10 rounded transition-all text-concrete hover:text-brand-accent"
title={t('wizard.title')}
>
<Sparkles size={14} />
</button>
<div className="relative">
<button
type="button"
@@ -1639,6 +1651,16 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
parentNotebookId={createParentId}
/>
{showAiWizard && (
<AiNotebookWizard
onClose={() => setShowAiWizard(false)}
onComplete={() => {
setShowAiWizard(false)
window.location.reload()
}}
/>
)}
{/* Rename Dialog */}
<AnimatePresence>
{renamingNotebook && (

View File

@@ -80,7 +80,7 @@ const CalloutView = ({ node, updateAttributes, deleteNode, getPos, editor }: any
}
return (
<NodeViewWrapper className="callout-block my-2" dir="auto">
<NodeViewWrapper className="callout-block group/callout my-2" dir="auto">
<div className={cn('flex items-start gap-2 rounded-lg border px-3 py-2.5', styles.bg, styles.border)}>
<div className="flex flex-col items-center gap-1 pt-0.5">
<CalloutTypePicker
@@ -91,7 +91,7 @@ const CalloutView = ({ node, updateAttributes, deleteNode, getPos, editor }: any
<div className="flex-1 min-w-0 text-sm leading-relaxed">
<NodeViewContent className="callout-content" />
</div>
<div className="flex flex-shrink-0 items-center gap-0.5 pt-0.5">
<div className="flex flex-shrink-0 items-center gap-0.5 pt-0.5 opacity-0 group-hover/callout:opacity-100 transition-opacity">
<button
onClick={unwrap}
contentEditable={false}

View File

@@ -33,7 +33,7 @@ const ToggleView = ({ node, updateAttributes, deleteNode, getPos, editor }: any)
}
return (
<NodeViewWrapper className="toggle-block my-1 rounded-lg border border-border/50 bg-muted/20" dir="auto">
<NodeViewWrapper className="toggle-block group/toggle my-1 rounded-lg border border-border/50 bg-muted/20" dir="auto">
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border/30">
<button
onClick={toggle}
@@ -53,22 +53,24 @@ const ToggleView = ({ node, updateAttributes, deleteNode, getPos, editor }: any)
>
{opened ? t('richTextEditor.toggleOpened') : t('richTextEditor.toggleClosed')}
</span>
<button
onClick={unwrap}
contentEditable={false}
className="flex-shrink-0 p-0.5 rounded hover:bg-muted text-muted-foreground"
title={t('richTextEditor.toggleUnwrap')}
>
<X className="h-3.5 w-3.5" />
</button>
<button
onClick={deleteNode}
contentEditable={false}
className="flex-shrink-0 p-0.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title={t('richTextEditor.toggleDelete')}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<div className="flex items-center gap-0.5 opacity-0 group-hover/toggle:opacity-100 transition-opacity">
<button
onClick={unwrap}
contentEditable={false}
className="p-0.5 rounded hover:bg-muted text-muted-foreground"
title={t('richTextEditor.toggleUnwrap')}
>
<X className="h-3.5 w-3.5" />
</button>
<button
onClick={deleteNode}
contentEditable={false}
className="p-0.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title={t('richTextEditor.toggleDelete')}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className={cn('px-3 py-2', !opened && 'hidden')}>

View File

@@ -0,0 +1,317 @@
'use client'
import { useState } from 'react'
import { GraduationCap, BookOpen, Wrench, Sparkles, Loader2, X, ArrowRight, ArrowLeft, Check, FileText } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
type WizardProfile = 'student' | 'teacher' | 'engineer'
interface ProfileOption {
id: WizardProfile
icon: typeof GraduationCap
titleKey: string
descKey: string
placeholderKey: string
}
const PROFILES: ProfileOption[] = [
{
id: 'student',
icon: GraduationCap,
titleKey: 'wizard.profileStudent',
descKey: 'wizard.profileStudentDesc',
placeholderKey: 'wizard.topicStudentPlaceholder',
},
{
id: 'teacher',
icon: BookOpen,
titleKey: 'wizard.profileTeacher',
descKey: 'wizard.profileTeacherDesc',
placeholderKey: 'wizard.topicTeacherPlaceholder',
},
{
id: 'engineer',
icon: Wrench,
titleKey: 'wizard.profileEngineer',
descKey: 'wizard.profileEngineerDesc',
placeholderKey: 'wizard.topicEngineerPlaceholder',
},
]
const LEVELS = ['wizard.levelBeginner', 'wizard.levelIntermediate', 'wizard.levelAdvanced', 'wizard.levelExpert']
export function AiNotebookWizard({ onClose, onComplete }: { onClose: () => void; onComplete: (notebookId: string) => void }) {
const { t, language } = useLanguage()
const [step, setStep] = useState<0 | 1 | 2>(0)
const [profile, setProfile] = useState<WizardProfile | null>(null)
const [topic, setTopic] = useState('')
const [level, setLevel] = useState(1)
const [count, setCount] = useState(6)
const [loading, setLoading] = useState(false)
const [progressMsg, setProgressMsg] = useState('')
const [success, setSuccess] = useState<{ notebookId: string; notebookName: string; noteTitles: string[] } | null>(null)
const handleSubmit = async () => {
if (!profile || !topic.trim()) return
setLoading(true)
setProgressMsg(t('wizard.progressGenerating') || 'Génération du contenu par l\'IA...')
try {
setProgressMsg(t('wizard.progressCalling') || 'Appel de l\'IA...')
const res = await fetch('/api/ai/notebook-wizard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
profile,
topic: topic.trim(),
level: t(LEVELS[level]),
count,
language,
notebookName: topic.trim(),
}),
})
setProgressMsg(t('wizard.progressParsing') || 'Analyse de la réponse...')
const data = await res.json()
if (!res.ok) {
if (data.errorKey === 'ai.featureLocked') {
toast.error(t('ai.featureLocked') || 'Cette fonctionnalité nécessite un plan supérieur')
} else if (data.errorKey === 'ai.quotaExceeded') {
toast.error(t('ai.quotaExceeded') || 'Quota IA dépassé')
} else {
toast.error(data.error || 'Erreur')
}
setLoading(false)
return
}
setProgressMsg(t('wizard.progressCreating') || 'Création du carnet et des notes...')
setSuccess({
notebookId: data.notebookId,
notebookName: data.notebookName || topic,
noteTitles: data.noteTitles || [],
})
setLoading(false)
} catch (e: any) {
toast.error(e.message || 'Erreur')
setLoading(false)
}
}
const selectedProfile = PROFILES.find(p => p.id === profile)
return (
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" dir="auto" onClick={onClose}>
<div
className="w-full max-w-2xl rounded-2xl border border-border bg-card shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-brand-accent/5">
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-brand-accent" />
<h2 className="text-base font-semibold">{t('wizard.title')}</h2>
</div>
<button onClick={onClose} className="p-1 rounded-lg hover:bg-muted text-muted-foreground">
<X className="h-5 w-5" />
</button>
</div>
{/* Progress bar */}
<div className="flex items-center px-6 pt-3 gap-2">
{[0, 1, 2].map(s => (
<div key={s} className="flex items-center gap-2 flex-1">
<div className={cn(
'w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold transition-colors flex-shrink-0',
s <= step ? 'bg-brand-accent text-white' : 'bg-border text-muted-foreground'
)}>
{s + 1}
</div>
{s < 2 && <div className={cn('h-0.5 flex-1 rounded-full transition-colors', s < step ? 'bg-brand-accent' : 'bg-border')} />}
</div>
))}
</div>
{/* Content */}
<div className="p-6 min-h-[360px]">
{success ? (
<div className="flex flex-col items-center py-6 gap-4">
<div className="w-14 h-14 rounded-full bg-green-100 dark:bg-green-950/40 flex items-center justify-center">
<Check className="h-7 w-7 text-green-600 dark:text-green-400" />
</div>
<div className="text-center">
<h3 className="text-base font-semibold mb-1">
{t('wizard.created') || 'Carnet créé :'} <span className="text-brand-accent">{success.notebookName}</span>
</h3>
<p className="text-sm text-muted-foreground">
{success.noteTitles.length} {t('wizard.notesCreated') || 'notes créées'}
</p>
</div>
<div className="w-full max-h-48 overflow-y-auto rounded-xl border border-border bg-muted/20 p-3 space-y-1.5">
{success.noteTitles.map((title, i) => (
<div key={i} className="flex items-center gap-2 text-sm py-1">
<FileText size={14} className="text-brand-accent flex-shrink-0" />
<span className="truncate">{title}</span>
</div>
))}
</div>
<button
onClick={() => onComplete(success.notebookId)}
className="flex items-center gap-2 px-5 py-2.5 text-sm rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 transition-colors font-medium"
>
<Sparkles className="h-4 w-4" />
{t('wizard.openNotebook') || 'Ouvrir le carnet'}
</button>
</div>
) : loading ? (
<div className="flex flex-col items-center justify-center py-16 gap-4">
<Loader2 className="h-10 w-10 animate-spin text-brand-accent" />
<p className="text-sm text-muted-foreground text-center max-w-sm">
{progressMsg}
</p>
<div className="flex flex-wrap gap-2 justify-center mt-2">
{['Toggle', 'Callout', 'Math', 'Colonnes', 'Sommaire', 'Link Preview'].map(tag => (
<span key={tag} className="text-[10px] px-2 py-0.5 rounded-full bg-brand-accent/10 text-brand-accent font-medium">
{tag}
</span>
))}
</div>
</div>
) : step === 0 ? (
/* Step 1: Choose profile */
<div className="space-y-3">
<p className="text-sm text-muted-foreground mb-4">{t('wizard.chooseProfile')}</p>
{PROFILES.map(p => (
<button
key={p.id}
onClick={() => { setProfile(p.id); setStep(1) }}
className="w-full flex items-start gap-3 p-4 rounded-xl border border-border hover:border-brand-accent/40 hover:bg-brand-accent/[0.03] transition-all text-left group"
>
<div className="w-10 h-10 rounded-lg bg-brand-accent/10 flex items-center justify-center flex-shrink-0 group-hover:bg-brand-accent/20 transition-colors">
<p.icon className="h-5 w-5 text-brand-accent" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold mb-0.5">{t(p.titleKey)}</h3>
<p className="text-xs text-muted-foreground leading-relaxed">{t(p.descKey)}</p>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground/40 group-hover:text-brand-accent transition-colors mt-1" />
</button>
))}
</div>
) : step === 1 ? (
/* Step 2: Topic + options */
<div className="space-y-5">
<div>
<label className="text-[10px] uppercase tracking-widest font-bold text-muted-foreground mb-2 block">
{t('wizard.topic')}
</label>
<input
type="text"
value={topic}
onChange={(e) => setTopic(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && topic.trim()) { setStep(2) } }}
placeholder={selectedProfile ? t(selectedProfile.placeholderKey) : ''}
autoFocus
className="w-full rounded-lg border border-border bg-background px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-brand-accent/30"
/>
</div>
<div>
<label className="text-[10px] uppercase tracking-widest font-bold text-muted-foreground mb-2 block">
{t('wizard.level')}
</label>
<div className="flex gap-2">
{LEVELS.map((lvlKey, i) => (
<button
key={lvlKey}
onClick={() => setLevel(i)}
className={cn(
'flex-1 py-2 rounded-lg text-xs font-medium border transition-all',
level === i
? 'border-brand-accent bg-brand-accent/10 text-brand-accent'
: 'border-border text-muted-foreground hover:border-foreground/20'
)}
>
{t(lvlKey)}
</button>
))}
</div>
</div>
<div>
<label className="text-[10px] uppercase tracking-widest font-bold text-muted-foreground mb-2 block">
{t('wizard.noteCount')}: {count}
</label>
<input
type="range"
min={3}
max={12}
value={count}
onChange={(e) => setCount(Number(e.target.value))}
className="w-full accent-brand-accent"
/>
<div className="flex justify-between text-[10px] text-muted-foreground mt-1">
<span>3</span>
<span>12</span>
</div>
</div>
<div className="flex justify-between pt-2">
<button onClick={() => setStep(0)} className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors">
<ArrowLeft className="h-4 w-4" />
{t('general.back') || 'Retour'}
</button>
<button
onClick={() => setStep(2)}
disabled={!topic.trim()}
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 disabled:opacity-40 transition-colors font-medium"
>
{t('general.continue') || 'Continuer'}
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
) : (
/* Step 3: Confirm */
<div className="space-y-5">
<div className="rounded-xl border border-border bg-muted/20 p-4 space-y-3">
<div className="flex items-center gap-2">
{selectedProfile && <selectedProfile.icon className="h-4 w-4 text-brand-accent" />}
<span className="text-sm font-medium">{selectedProfile && t(selectedProfile.titleKey)}</span>
</div>
<div className="space-y-1.5 text-sm">
<div className="flex justify-between"><span className="text-muted-foreground">{t('wizard.topic')}</span><span className="font-medium">{topic}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">{t('wizard.level')}</span><span className="font-medium">{t(LEVELS[level])}</span></div>
<div className="flex justify-between"><span className="text-muted-foreground">{t('wizard.noteCount')}</span><span className="font-medium">{count} {t('wizard.notes')}</span></div>
</div>
</div>
<div className="rounded-lg bg-brand-accent/5 border border-brand-accent/20 p-3">
<p className="text-xs text-muted-foreground leading-relaxed">
{t('wizard.confirmHint') || 'L\'IA va créer un carnet avec des notes riches : encadrés, sections repliables, formules mathématiques, colonnes de comparaison, et plus.'}
</p>
</div>
<div className="flex justify-between pt-2">
<button onClick={() => setStep(1)} className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors">
<ArrowLeft className="h-4 w-4" />
{t('general.back') || 'Retour'}
</button>
<button
onClick={handleSubmit}
className="flex items-center gap-2 px-5 py-2.5 text-sm rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 transition-colors font-medium"
>
<Sparkles className="h-4 w-4" />
{t('wizard.generate') || 'Générer le carnet'}
</button>
</div>
</div>
)}
</div>
</div>
</div>
)
}