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:
@@ -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')}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
317
memento-note/components/wizard/ai-notebook-wizard.tsx
Normal file
317
memento-note/components/wizard/ai-notebook-wizard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user