feat: AI Writer inline — génère du contenu au curseur
- Mode 'write' ajouté à paragraph-refactor.service.ts - Endpoint /api/ai/reformulate étendu (option 'write' + writePrompt) - UI : champ de prompt inline apparaît au curseur après slash menu - Tape / → 'Écrire avec l'IA' → décrit ce que tu veux → Entrée - L'IA génère et insère le contenu à la position du curseur - Pas de migration DB, réutilise l'infra existante - i18n FR/EN
This commit is contained in:
@@ -109,7 +109,7 @@ type SlashItem = {
|
||||
shortcut?: string
|
||||
isImage?: boolean
|
||||
isAi?: boolean
|
||||
aiOption?: 'clarify' | 'shorten' | 'improve'
|
||||
aiOption?: 'clarify' | 'shorten' | 'improve' | 'write'
|
||||
command: (editor: Editor, range?: any) => void
|
||||
}
|
||||
|
||||
@@ -173,7 +173,12 @@ const slashCommands: SlashItem[] = [
|
||||
{ title: 'Clarifier', description: 'Rendre le texte plus clair', icon: Lightbulb, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { } },
|
||||
{ title: 'Raccourcir', description: 'Condenser le texte', icon: Scissors, category: 'IA Note', isAi: true, aiOption: 'shorten', command: () => { } },
|
||||
{ title: 'Améliorer', description: 'Améliorer le style', icon: Wand2, category: 'IA Note', isAi: true, aiOption: 'improve', command: () => { } },
|
||||
{ title: 'Développer', description: 'Élaborer et enrichir le texte', icon: Expand, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { } },
|
||||
{
|
||||
title: 'Développer', description: 'Élaborer et enrichir le texte', icon: Expand, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { }
|
||||
},
|
||||
{
|
||||
title: 'Écrire avec l\'IA', description: 'Générer du contenu au curseur', icon: Sparkles, category: 'IA Note', isAi: true, aiOption: 'write', command: () => { }
|
||||
},
|
||||
// Formatting extensions
|
||||
{ title: 'Bold', description: 'Make text bold', icon: Bold, category: 'Formatting', command: (e) => e.chain().focus().toggleBold().run() },
|
||||
{ title: 'Italic', description: 'Make text italic', icon: Italic, category: 'Formatting', command: (e) => e.chain().focus().toggleItalic().run() },
|
||||
@@ -1672,6 +1677,9 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
||||
const [coords, setCoords] = useState({ top: 0, left: 0 })
|
||||
const [previewCoords, setPreviewCoords] = useState({ top: 0, left: 0, side: 'right' as 'right' | 'left' })
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const [aiWriterOpen, setAiWriterOpen] = useState(false)
|
||||
const [aiWriterPrompt, setAiWriterPrompt] = useState('')
|
||||
const [aiWriterLoading, setAiWriterLoading] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const selectedItemRef = useRef<HTMLButtonElement>(null)
|
||||
const menuInteracting = useRef(false)
|
||||
@@ -1715,6 +1723,7 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
||||
{ ...slashCommands[34], title: t('richTextEditor.slashLinkPreview'), description: t('richTextEditor.slashLinkPreviewDesc'), categoryId: 'embed', slashKeywords: ['link', 'lien', 'url', 'preview', 'apercu', 'aperçu', 'embed', 'card', 'carte'] },
|
||||
{ ...slashCommands[35], title: t('richTextEditor.slashMath'), description: t('richTextEditor.slashMathDesc'), categoryId: 'text', slashKeywords: ['math', 'maths', 'equation', 'équation', 'formula', 'formule', 'latex', 'katex'] },
|
||||
{ ...slashCommands[36], title: t('richTextEditor.slashColumns'), description: t('richTextEditor.slashColumnsDesc'), categoryId: 'text', slashKeywords: ['columns', 'colonnes', 'cols', 'layout', 'mise', 'page', 'cote', 'côte'] },
|
||||
{ ...slashCommands[37], title: t('richTextEditor.slashAiWriter') || 'Écrire avec l\'IA', description: t('richTextEditor.slashAiWriterDesc') || 'Générer du contenu au curseur', categoryId: 'ai', slashKeywords: ['ecrire', 'écrire', 'write', 'ia', 'ai', 'generer', 'générer', 'rediger', 'rédiger'] },
|
||||
{
|
||||
title: t('richTextEditor.slashNoteLink'),
|
||||
description: t('richTextEditor.slashNoteLinkDesc'),
|
||||
@@ -1757,6 +1766,10 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
||||
}
|
||||
if (item.isImage) {
|
||||
deleteSlashText(); closeMenu(); onInsertImage(editor)
|
||||
} else if (item.isAi && item.aiOption === 'write') {
|
||||
deleteSlashText(); closeMenu()
|
||||
setAiWriterOpen(true)
|
||||
setAiWriterPrompt('')
|
||||
} else if (item.isAi && item.aiOption) {
|
||||
deleteSlashText(); closeMenu(); setAiLoading(true)
|
||||
try {
|
||||
@@ -1784,6 +1797,43 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
||||
}
|
||||
}, [editor, closeMenu, deleteSlashText, onInsertImage, onSuggestCharts, t, requestAiConsent])
|
||||
|
||||
const handleAiWriterSubmit = useCallback(async () => {
|
||||
if (!aiWriterPrompt.trim() || !editor) return
|
||||
setAiWriterLoading(true)
|
||||
try {
|
||||
const consented = await requestAiConsent()
|
||||
if (!consented) return
|
||||
|
||||
const noteTitle = (editor.storage as any).structuredViewBlock?.noteTitle || ''
|
||||
const res = await fetch('/api/ai/reformulate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: noteTitle,
|
||||
option: 'write',
|
||||
format: 'html',
|
||||
language: 'Francais',
|
||||
writePrompt: aiWriterPrompt.trim(),
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
toast.error(data.error || 'Erreur')
|
||||
return
|
||||
}
|
||||
const html = data.reformulatedText || data.text || ''
|
||||
if (html) {
|
||||
editor.chain().focus().insertContent(html).run()
|
||||
}
|
||||
setAiWriterOpen(false)
|
||||
setAiWriterPrompt('')
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Erreur')
|
||||
} finally {
|
||||
setAiWriterLoading(false)
|
||||
}
|
||||
}, [aiWriterPrompt, editor, requestAiConsent])
|
||||
|
||||
// Charger les favoris fréquents lors de l'ouverture
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
@@ -2071,6 +2121,51 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
||||
<SlashPreview itemTitle={selectedItem.title} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{aiWriterOpen && createPortal(
|
||||
<div
|
||||
className="fixed z-[9999] flex items-center gap-2 rounded-xl border border-brand-accent/30 bg-card shadow-xl px-3 py-2"
|
||||
style={{
|
||||
top: (() => {
|
||||
const { from } = editor.state.selection
|
||||
const c = editor.view.coordsAtPos(from)
|
||||
return c.bottom + 4
|
||||
})(),
|
||||
left: (() => {
|
||||
const { from } = editor.state.selection
|
||||
const c = editor.view.coordsAtPos(from)
|
||||
return Math.min(c.left, window.innerWidth - 420)
|
||||
})(),
|
||||
}}
|
||||
dir="auto"
|
||||
>
|
||||
<Sparkles size={16} className="text-brand-accent flex-shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
value={aiWriterPrompt}
|
||||
onChange={(e) => setAiWriterPrompt(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Enter') { e.preventDefault(); handleAiWriterSubmit() }
|
||||
if (e.key === 'Escape') { setAiWriterOpen(false); setAiWriterPrompt('') }
|
||||
}}
|
||||
autoFocus
|
||||
disabled={aiWriterLoading}
|
||||
placeholder={t('richTextEditor.aiWriterPlaceholder') || 'Décris ce que tu veux écrire...'}
|
||||
className="flex-1 min-w-[300px] bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
{aiWriterLoading && <Loader2 size={14} className="animate-spin text-brand-accent flex-shrink-0" />}
|
||||
{!aiWriterLoading && aiWriterPrompt.trim() && (
|
||||
<button
|
||||
onClick={handleAiWriterSubmit}
|
||||
className="text-xs font-medium text-brand-accent hover:underline flex-shrink-0"
|
||||
>
|
||||
{t('general.send') || '→'}
|
||||
</button>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>,
|
||||
document.body
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user