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:
@@ -23,32 +23,29 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 })
|
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, option, format, language } = await request.json()
|
const { text, option, format, language, writePrompt } = await request.json()
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!text || typeof text !== 'string') {
|
|
||||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map option to refactor mode
|
// Map option to refactor mode
|
||||||
const modeMap: Record<string, 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain'> = {
|
const modeMap: Record<string, 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain' | 'write'> = {
|
||||||
'clarify': 'clarify',
|
'clarify': 'clarify',
|
||||||
'shorten': 'shorten',
|
'shorten': 'shorten',
|
||||||
'improve': 'improveStyle',
|
'improve': 'improveStyle',
|
||||||
'fix_grammar': 'fix_grammar',
|
'fix_grammar': 'fix_grammar',
|
||||||
'translate': 'translate',
|
'translate': 'translate',
|
||||||
'explain': 'explain'
|
'explain': 'explain',
|
||||||
|
'write': 'write'
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = modeMap[option]
|
const mode = modeMap[option]
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Invalid option. Use: clarify, shorten, improve, fix_grammar, translate, or explain' },
|
{ error: 'Invalid option. Use: clarify, shorten, improve, fix_grammar, translate, explain, or write' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate word count
|
// Validate word count (skip for 'write' mode — no selection needed)
|
||||||
|
if (mode !== 'write') {
|
||||||
const validation = paragraphRefactorService.validateWordCount(text)
|
const validation = paragraphRefactorService.validateWordCount(text)
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -57,6 +54,7 @@ export async function POST(request: NextRequest) {
|
|||||||
params: validation.params,
|
params: validation.params,
|
||||||
}, { status: 400 })
|
}, { status: 400 })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check quota
|
// Check quota
|
||||||
try {
|
try {
|
||||||
@@ -75,7 +73,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the ParagraphRefactorService
|
// Use the ParagraphRefactorService
|
||||||
const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown', language)
|
const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown', language, writePrompt)
|
||||||
|
|
||||||
incrementUsageAsync(session.user.id, 'reformulate')
|
incrementUsageAsync(session.user.id, 'reformulate')
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ type SlashItem = {
|
|||||||
shortcut?: string
|
shortcut?: string
|
||||||
isImage?: boolean
|
isImage?: boolean
|
||||||
isAi?: boolean
|
isAi?: boolean
|
||||||
aiOption?: 'clarify' | 'shorten' | 'improve'
|
aiOption?: 'clarify' | 'shorten' | 'improve' | 'write'
|
||||||
command: (editor: Editor, range?: any) => void
|
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: '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: '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: '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
|
// Formatting extensions
|
||||||
{ title: 'Bold', description: 'Make text bold', icon: Bold, category: 'Formatting', command: (e) => e.chain().focus().toggleBold().run() },
|
{ 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() },
|
{ 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 [coords, setCoords] = useState({ top: 0, left: 0 })
|
||||||
const [previewCoords, setPreviewCoords] = useState({ top: 0, left: 0, side: 'right' as 'right' | 'left' })
|
const [previewCoords, setPreviewCoords] = useState({ top: 0, left: 0, side: 'right' as 'right' | 'left' })
|
||||||
const [aiLoading, setAiLoading] = useState(false)
|
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 menuRef = useRef<HTMLDivElement>(null)
|
||||||
const selectedItemRef = useRef<HTMLButtonElement>(null)
|
const selectedItemRef = useRef<HTMLButtonElement>(null)
|
||||||
const menuInteracting = useRef(false)
|
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[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[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[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'),
|
title: t('richTextEditor.slashNoteLink'),
|
||||||
description: t('richTextEditor.slashNoteLinkDesc'),
|
description: t('richTextEditor.slashNoteLinkDesc'),
|
||||||
@@ -1757,6 +1766,10 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
|||||||
}
|
}
|
||||||
if (item.isImage) {
|
if (item.isImage) {
|
||||||
deleteSlashText(); closeMenu(); onInsertImage(editor)
|
deleteSlashText(); closeMenu(); onInsertImage(editor)
|
||||||
|
} else if (item.isAi && item.aiOption === 'write') {
|
||||||
|
deleteSlashText(); closeMenu()
|
||||||
|
setAiWriterOpen(true)
|
||||||
|
setAiWriterPrompt('')
|
||||||
} else if (item.isAi && item.aiOption) {
|
} else if (item.isAi && item.aiOption) {
|
||||||
deleteSlashText(); closeMenu(); setAiLoading(true)
|
deleteSlashText(); closeMenu(); setAiLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -1784,6 +1797,43 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
|||||||
}
|
}
|
||||||
}, [editor, closeMenu, deleteSlashText, onInsertImage, onSuggestCharts, t, requestAiConsent])
|
}, [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
|
// Charger les favoris fréquents lors de l'ouverture
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return
|
if (!isOpen) return
|
||||||
@@ -2071,6 +2121,51 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
|||||||
<SlashPreview itemTitle={selectedItem.title} />
|
<SlashPreview itemTitle={selectedItem.title} />
|
||||||
</div>
|
</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
|
document.body
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { LanguageDetectionService } from './language-detection.service'
|
|||||||
import { getTagsProvider } from '../factory'
|
import { getTagsProvider } from '../factory'
|
||||||
import { getSystemConfig } from '@/lib/config'
|
import { getSystemConfig } from '@/lib/config'
|
||||||
|
|
||||||
export type RefactorMode = 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain'
|
export type RefactorMode = 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain' | 'write'
|
||||||
|
|
||||||
export interface RefactorOption {
|
export interface RefactorOption {
|
||||||
mode: RefactorMode
|
mode: RefactorMode
|
||||||
@@ -69,24 +69,29 @@ export class ParagraphRefactorService {
|
|||||||
content: string,
|
content: string,
|
||||||
mode: RefactorMode,
|
mode: RefactorMode,
|
||||||
format: 'html' | 'markdown' = 'markdown',
|
format: 'html' | 'markdown' = 'markdown',
|
||||||
targetLanguage?: string
|
targetLanguage?: string,
|
||||||
|
writePrompt?: string
|
||||||
): Promise<RefactorResult> {
|
): Promise<RefactorResult> {
|
||||||
// Validate word count
|
// 'write' mode skips word count validation (generates from prompt, no selection needed)
|
||||||
|
if (mode !== 'write') {
|
||||||
const wordCount = content.split(/\s+/).length
|
const wordCount = content.split(/\s+/).length
|
||||||
if (wordCount < this.MIN_WORDS || wordCount > this.MAX_WORDS) {
|
if (wordCount < this.MIN_WORDS || wordCount > this.MAX_WORDS) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Please select ${this.MIN_WORDS}-${this.MAX_WORDS} words to reformulate`
|
`Please select ${this.MIN_WORDS}-${this.MAX_WORDS} words to reformulate`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Detect language or use provided target language
|
// Detect language or use provided target language
|
||||||
const { language: detectedLanguage } = await this.languageDetection.detectLanguage(content)
|
const { language: detectedLanguage } = mode === 'write'
|
||||||
|
? { language: targetLanguage || 'fr' }
|
||||||
|
: await this.languageDetection.detectLanguage(content)
|
||||||
const language = targetLanguage || detectedLanguage
|
const language = targetLanguage || detectedLanguage
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build prompts
|
// Build prompts
|
||||||
const systemPrompt = this.getSystemPrompt(mode, format)
|
const systemPrompt = this.getSystemPrompt(mode, format)
|
||||||
const userPrompt = this.getUserPrompt(mode, content, language, format)
|
const userPrompt = this.getUserPrompt(mode, content, language, format, writePrompt)
|
||||||
|
|
||||||
// Get AI provider from factory
|
// Get AI provider from factory
|
||||||
const config = await getSystemConfig()
|
const config = await getSystemConfig()
|
||||||
@@ -99,10 +104,10 @@ export class ParagraphRefactorService {
|
|||||||
// Calculate word count change
|
// Calculate word count change
|
||||||
const refactoredWordCount = refactored.split(/\s+/).length
|
const refactoredWordCount = refactored.split(/\s+/).length
|
||||||
const wordCountChange = {
|
const wordCountChange = {
|
||||||
original: wordCount,
|
original: mode === 'write' ? 0 : content.split(/\s+/).length,
|
||||||
refactored: refactoredWordCount,
|
refactored: refactoredWordCount,
|
||||||
difference: refactoredWordCount - wordCount,
|
difference: refactoredWordCount - (mode === 'write' ? 0 : content.split(/\s+/).length),
|
||||||
percentage: ((refactoredWordCount - wordCount) / wordCount) * 100
|
percentage: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -252,7 +257,13 @@ Your goal: Explain the selected text, concept, or word clearly and concisely.
|
|||||||
Provide context, definitions, or relevant information to help the user understand it.
|
Provide context, definitions, or relevant information to help the user understand it.
|
||||||
|
|
||||||
CRITICAL LANGUAGE RULE: You MUST explain it in the requested language (which is the user's interface language).
|
CRITICAL LANGUAGE RULE: You MUST explain it in the requested language (which is the user's interface language).
|
||||||
Keep it concise but informative.${formatInstruction}`
|
Keep it concise but informative.${formatInstruction}`,
|
||||||
|
|
||||||
|
write: `You are an expert writer and content creator.
|
||||||
|
Your goal: Write new content based on the user's instruction. Be clear, well-structured, and informative.
|
||||||
|
Write naturally in the requested language.
|
||||||
|
|
||||||
|
If context is provided (note title or surrounding text), use it to make the content relevant.${formatInstruction}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return prompts[mode]
|
return prompts[mode]
|
||||||
@@ -261,7 +272,7 @@ Keep it concise but informative.${formatInstruction}`
|
|||||||
/**
|
/**
|
||||||
* Get mode-specific user prompt
|
* Get mode-specific user prompt
|
||||||
*/
|
*/
|
||||||
private getUserPrompt(mode: RefactorMode, content: string, language: string, format: 'html' | 'markdown' = 'markdown'): string {
|
private getUserPrompt(mode: RefactorMode, content: string, language: string, format: 'html' | 'markdown' = 'markdown', writePrompt?: string): string {
|
||||||
const instructions = {
|
const instructions = {
|
||||||
clarify: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
|
clarify: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
|
||||||
|
|
||||||
@@ -283,7 +294,20 @@ Please fix any spelling, grammar, or punctuation errors in this ${language} text
|
|||||||
Only return the translated text, nothing else.`,
|
Only return the translated text, nothing else.`,
|
||||||
|
|
||||||
explain: `Please explain the following text/concept in ${language}.
|
explain: `Please explain the following text/concept in ${language}.
|
||||||
Keep the explanation clear, educational, and concise.`
|
Keep the explanation clear, educational, and concise.`,
|
||||||
|
|
||||||
|
write: `Write the following in ${language}. Only return the content, no meta-commentary.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'write') {
|
||||||
|
const suffix = `CRITICAL: Respond ONLY with the generated content in ${language}. No explanations, no meta-commentary. Format strictly as ${format === 'html' ? 'HTML tags' : 'Markdown'}.`
|
||||||
|
const contextPart = content.trim() ? `\n\nContext (note: "${content.trim().slice(0, 100)}"):\n` : ''
|
||||||
|
return `${instructions.write}
|
||||||
|
|
||||||
|
${writePrompt || 'Write a paragraph about the current topic.'}
|
||||||
|
|
||||||
|
${contextPart}
|
||||||
|
${suffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemSuffix = mode === 'explain'
|
const systemSuffix = mode === 'explain'
|
||||||
|
|||||||
@@ -2560,6 +2560,9 @@
|
|||||||
"pdfExportSuccess": "PDF ready!",
|
"pdfExportSuccess": "PDF ready!",
|
||||||
"generateExercises": "Generate exercises",
|
"generateExercises": "Generate exercises",
|
||||||
"generateExercisesHint": "5 exercises + answers",
|
"generateExercisesHint": "5 exercises + answers",
|
||||||
|
"slashAiWriter": "Write with AI",
|
||||||
|
"slashAiWriterDesc": "Generate content at cursor",
|
||||||
|
"aiWriterPlaceholder": "Describe what you want to write...",
|
||||||
"exercisesLoading": "Generating exercises...",
|
"exercisesLoading": "Generating exercises...",
|
||||||
"exercisesGenerated": "exercises created!",
|
"exercisesGenerated": "exercises created!",
|
||||||
"aiGenerateExercises": "Generate exercises",
|
"aiGenerateExercises": "Generate exercises",
|
||||||
|
|||||||
@@ -2564,6 +2564,9 @@
|
|||||||
"pdfExportSuccess": "PDF prêt !",
|
"pdfExportSuccess": "PDF prêt !",
|
||||||
"generateExercises": "Générer des exercices",
|
"generateExercises": "Générer des exercices",
|
||||||
"generateExercisesHint": "5 exercices + corrigés",
|
"generateExercisesHint": "5 exercices + corrigés",
|
||||||
|
"slashAiWriter": "Écrire avec l'IA",
|
||||||
|
"slashAiWriterDesc": "Générer du contenu au curseur",
|
||||||
|
"aiWriterPlaceholder": "Décris ce que tu veux écrire...",
|
||||||
"exercisesLoading": "Génération des exercices...",
|
"exercisesLoading": "Génération des exercices...",
|
||||||
"exercisesGenerated": "exercices créés !",
|
"exercisesGenerated": "exercices créés !",
|
||||||
"aiGenerateExercises": "Générer des exercices",
|
"aiGenerateExercises": "Générer des exercices",
|
||||||
|
|||||||
Reference in New Issue
Block a user