feat: icon-only toolbar, versioning fixes, history modal, PanelRight repositioning
- Toolbar: remove text labels from all icon buttons (AI, Save, Preview, Convert) all buttons now icon-only with title tooltip for accessibility - Toolbar: reposition PanelRight (info panel toggle) to far right after three-dot menu - Versioning: decouple getNoteHistory/restoreNoteVersion from global userAISettings.noteHistory now checks note.historyEnabled directly — unblocks manual per-note history - Versioning: add 'Sauvegarder cette version' button in Versions tab of info panel calls commitNoteHistory with visual feedback (spinner → success state) - note-document-info-panel: import commitNoteHistory, add isSavingVersion state - notes.ts: fix double guard that silently blocked all history operations
This commit is contained in:
@@ -10,11 +10,12 @@ interface NoteEditorProps {
|
||||
readOnly?: boolean
|
||||
onClose: () => void
|
||||
fullPage?: boolean
|
||||
onNoteSaved?: (savedNote: Note) => void
|
||||
}
|
||||
|
||||
export function NoteEditor({ note, readOnly, onClose, fullPage = false }: NoteEditorProps) {
|
||||
export function NoteEditor({ note, readOnly, onClose, fullPage = false, onNoteSaved }: NoteEditorProps) {
|
||||
return (
|
||||
<NoteEditorProvider note={note} readOnly={readOnly} fullPage={fullPage}>
|
||||
<NoteEditorProvider note={note} readOnly={readOnly} fullPage={fullPage} onNoteSaved={onNoteSaved}>
|
||||
{fullPage ? (
|
||||
<NoteEditorFullPage onClose={onClose} />
|
||||
) : (
|
||||
|
||||
@@ -25,10 +25,11 @@ interface NoteEditorProviderProps {
|
||||
note: Note
|
||||
readOnly?: boolean
|
||||
fullPage?: boolean
|
||||
onNoteSaved?: (savedNote: Note) => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function NoteEditorProvider({ note, readOnly = false, fullPage = false, children }: NoteEditorProviderProps) {
|
||||
export function NoteEditorProvider({ note, readOnly = false, fullPage = false, onNoteSaved, children }: NoteEditorProviderProps) {
|
||||
const { data: session } = useSession()
|
||||
const { t } = useLanguage()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -133,6 +134,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
enabled: fullPage && !title && !dismissedTitleSuggestions,
|
||||
})
|
||||
|
||||
// Wire autoTitleSuggestions into state so NoteTitleBlock can display them
|
||||
useEffect(() => {
|
||||
if (autoTitleSuggestions.length > 0) {
|
||||
setTitleSuggestions(autoTitleSuggestions)
|
||||
}
|
||||
}, [autoTitleSuggestions])
|
||||
|
||||
// Track previous content for copilot action undo
|
||||
const [previousContentForCopilot, setPreviousContentForCopilot] = useState<string | null>(null)
|
||||
|
||||
@@ -169,7 +177,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
for (const file of Array.from(files)) {
|
||||
try {
|
||||
const url = await uploadImageFile(file)
|
||||
setImages(prev => [...prev, url])
|
||||
setImages(prev => prev.includes(url) ? prev : [...prev, url])
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
toast.error(t('notes.uploadFailed', { filename: file.name }))
|
||||
@@ -190,7 +198,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
if (!file) continue
|
||||
try {
|
||||
const url = await uploadImageFile(file)
|
||||
setImages(prev => [...prev, url])
|
||||
setImages(prev => prev.includes(url) ? prev : [...prev, url])
|
||||
} catch {
|
||||
toast.error(t('notes.uploadFailed', { filename: 'pasted image' }))
|
||||
}
|
||||
@@ -293,7 +301,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
|
||||
const data = await response.json()
|
||||
setTitleSuggestions(data.suggestions || [])
|
||||
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
|
||||
// Auto-apply first title for dialog mode (fullPage shows suggestions UI instead)
|
||||
if (!fullPage && data.suggestions?.[0]?.title) {
|
||||
setTitle(data.suggestions[0].title)
|
||||
setDismissedTitleSuggestions(true)
|
||||
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
|
||||
} else if (data.suggestions?.length) {
|
||||
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error generating titles:', error)
|
||||
toast.error(error.message || t('ai.titleGenerationFailed'))
|
||||
@@ -521,9 +536,11 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
console.log('[SAVE] handleSave called, note.id:', note.id)
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await updateNote(note.id, {
|
||||
console.log('[SAVE] Calling updateNote...')
|
||||
const result = await updateNote(note.id, {
|
||||
title: title.trim() || null,
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
checkItems: noteType === 'checklist' ? checkItems : null,
|
||||
@@ -536,20 +553,25 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
type: noteType,
|
||||
size,
|
||||
})
|
||||
|
||||
console.log('[SAVE] updateNote succeeded, result title:', result?.title, 'result content len:', result?.content?.length)
|
||||
console.log('[SAVE] prevNoteRef BEFORE sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 80))
|
||||
// Keep local note ref in sync with saved data so useEffect detects changes correctly
|
||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||
console.log('[SAVE] prevNoteRef AFTER sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 80))
|
||||
if (removedImageUrls.length > 0) {
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
}
|
||||
|
||||
await refreshLabels()
|
||||
// Notify parent with the freshly-saved note so it can update its local state immediately
|
||||
onNoteSaved?.(result)
|
||||
// Invalidate note and notes list cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
|
||||
triggerRefresh()
|
||||
|
||||
// Note: onClose is handled by the composition component
|
||||
toast.success(t('notes.saved') || 'Note sauvegardée !')
|
||||
} catch (error) {
|
||||
console.error('Failed to save note:', error)
|
||||
console.error('[SAVE] updateNote failed:', error)
|
||||
toast.error(t('notes.saveFailed') || 'Erreur lors de la sauvegarde.')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -633,9 +655,11 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
|
||||
// Save in place (fullPage) — without closing
|
||||
const handleSaveInPlace = async () => {
|
||||
console.log('[SAVE] handleSaveInPlace called, note.id:', note.id, 'content length:', content.length, 'title:', title.substring(0, 50))
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await updateNote(note.id, {
|
||||
console.log('[SAVE] Calling updateNote with note.id:', note.id, '| content len:', content.length, '| title:', title.substring(0, 30))
|
||||
const updatePayload = {
|
||||
title: title.trim() || null,
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
checkItems: noteType === 'checklist' ? checkItems : null,
|
||||
@@ -647,11 +671,20 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
isMarkdown: noteType === 'markdown',
|
||||
type: noteType,
|
||||
size,
|
||||
})
|
||||
}
|
||||
console.log('[SAVE] payload.content:', JSON.stringify(updatePayload.content)?.substring(0, 100))
|
||||
const result = await updateNote(note.id, updatePayload)
|
||||
console.log('[SAVE] updateNote succeeded, result.id:', result?.id, '| result.content len:', result?.content?.length, '| result.title:', result?.title)
|
||||
console.log('[SAVE] prevNoteRef BEFORE sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 50))
|
||||
// Sync local note reference with saved data so prop/state stay aligned after save
|
||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||
console.log('[SAVE] prevNoteRef AFTER sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 50))
|
||||
if (removedImageUrls.length > 0) {
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
}
|
||||
await refreshLabels()
|
||||
// Notify parent with the freshly-saved note so it can update its local state immediately
|
||||
onNoteSaved?.(result)
|
||||
// Invalidate note and notes list cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
|
||||
@@ -659,7 +692,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
setIsDirty(false)
|
||||
toast.success('Note sauvegardée !')
|
||||
} catch (error) {
|
||||
console.error('Failed to save note:', error)
|
||||
console.error('[SAVE] updateNote failed:', error)
|
||||
toast.error('Erreur lors de la sauvegarde.')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
@@ -725,8 +758,11 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
|
||||
])
|
||||
|
||||
// Build actions object
|
||||
const actions: NoteEditorActions = useMemo(() => ({
|
||||
// Build actions object — NOT memoized to avoid stale closures.
|
||||
// handleSave / handleSaveInPlace close over content, title, labels, etc.
|
||||
// which change on every keystroke. Memoizing with [] would freeze those
|
||||
// values at the first render, causing the wrong content to be saved.
|
||||
const actions: NoteEditorActions = {
|
||||
setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
|
||||
setDismissedTitleSuggestions,
|
||||
setContent: (c) => { setContent(c); setIsDirty(true) },
|
||||
@@ -775,9 +811,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
|
||||
setInfoOpen,
|
||||
setIsProcessingAI,
|
||||
setIsGeneratingTitles,
|
||||
setIsAnalyzingSuggestions: (a) => { /* handled by useAutoTagging */ },
|
||||
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
|
||||
setPreviousContentForCopilot,
|
||||
}), [])
|
||||
}
|
||||
|
||||
|
||||
const value: NoteEditorContextValue = useMemo(() => ({
|
||||
note,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { useNoteEditorContext } from './note-editor-context'
|
||||
import { LabelManager } from '@/components/label-manager'
|
||||
import { LabelBadge } from '@/components/label-badge'
|
||||
@@ -17,11 +18,11 @@ import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
|
||||
Maximize2, Copy, ArrowLeft, ChevronRight, Info, Check, Loader2, Save, MoreHorizontal,
|
||||
Trash2, LogOut
|
||||
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
|
||||
Trash2, LogOut, Wand2
|
||||
} from 'lucide-react'
|
||||
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useRefresh } from '@/lib/use-refresh'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { NOTE_COLORS, NoteColor, Note } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -36,10 +37,59 @@ interface NoteEditorToolbarProps {
|
||||
export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
const { state, actions, note, readOnly, fullPage, notebooks, fileInputRef } = useNoteEditorContext()
|
||||
const { t } = useLanguage()
|
||||
const { triggerRefresh } = useNoteRefresh()
|
||||
const { refreshNotes } = useRefresh()
|
||||
const [isConverting, setIsConverting] = useState(false)
|
||||
|
||||
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
||||
|
||||
// Snapshot for undo — stored in a ref so the toast callback isn't a stale closure
|
||||
const undoSnapshotRef = useRef<{ content: string; noteType: string } | null>(null)
|
||||
|
||||
const handleConvertToRichtext = async () => {
|
||||
if (isConverting || !state.content.trim()) return
|
||||
setIsConverting(true)
|
||||
|
||||
// Capture snapshot BEFORE converting
|
||||
const snapshot = { content: state.content, noteType: state.noteType }
|
||||
undoSnapshotRef.current = snapshot
|
||||
|
||||
try {
|
||||
let html: string
|
||||
if (state.noteType === 'markdown') {
|
||||
// Proper markdown → HTML via marked (no AI needed)
|
||||
const { marked } = await import('marked')
|
||||
html = await marked(state.content, { async: false }) as string
|
||||
} else {
|
||||
// Plain text → wrap paragraphs in <p> tags
|
||||
html = state.content
|
||||
.split(/\n{2,}/)
|
||||
.map(para => `<p>${para.trim().replace(/\n/g, '<br />')}</p>`)
|
||||
.join('')
|
||||
}
|
||||
actions.setContent(html)
|
||||
actions.setNoteType('richtext')
|
||||
|
||||
toast.success(t('notes.convertedToRichText') || 'Converted to rich text', {
|
||||
duration: 8000,
|
||||
action: {
|
||||
label: t('notes.undo') || '↩ Undo',
|
||||
onClick: () => {
|
||||
const snap = undoSnapshotRef.current
|
||||
if (!snap) return
|
||||
actions.setContent(snap.content)
|
||||
actions.setNoteType(snap.noteType as any)
|
||||
undoSnapshotRef.current = null
|
||||
toast.info(t('ai.undoApplied') || 'Conversion undone')
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
toast.error(t('notes.transformFailed') || 'Conversion failed')
|
||||
} finally {
|
||||
setIsConverting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'fullPage') {
|
||||
return (
|
||||
<div className="px-12 py-8 flex items-center justify-between sticky top-0 bg-white/95 dark:bg-zinc-950/95 backdrop-blur-sm z-40 border-b border-border dark:border-white/10">
|
||||
@@ -70,68 +120,70 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
compact
|
||||
/>
|
||||
|
||||
{/* Preview toggle — only for text/markdown, in toolbar where it's visible */}
|
||||
{/* Preview toggle — icon only */}
|
||||
{(state.noteType === 'text' || state.noteType === 'markdown') && !readOnly && (
|
||||
<button
|
||||
title={state.showMarkdownPreview ? 'Revenir à l\'édition' : 'Aperçu'}
|
||||
aria-label={state.showMarkdownPreview ? 'Revenir à l\'édition' : 'Prévisualiser le rendu'}
|
||||
onClick={() => actions.setShowMarkdownPreview(!state.showMarkdownPreview)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
|
||||
'p-1.5 rounded-full border transition-all duration-300',
|
||||
state.showMarkdownPreview
|
||||
? 'bg-foreground text-background border-foreground'
|
||||
: 'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Eye size={16} />
|
||||
<span>{state.showMarkdownPreview ? 'Éditer' : 'Aperçu'}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* AI — rounded-full, exact prototype style */}
|
||||
{/* Convert to Rich Text — icon only */}
|
||||
{(state.noteType === 'text' || state.noteType === 'markdown') && !readOnly && (
|
||||
<button
|
||||
title={t('ai.convertToRichtext') || 'Convert to Rich Text'}
|
||||
aria-label={t('ai.convertToRichtext') || 'Convert to Rich Text'}
|
||||
onClick={handleConvertToRichtext}
|
||||
disabled={isConverting}
|
||||
className={cn(
|
||||
'p-1.5 rounded-full border transition-all duration-300',
|
||||
'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5',
|
||||
isConverting && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isConverting ? <Loader2 size={14} className="animate-spin" /> : <Wand2 size={14} />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* AI — icon only */}
|
||||
<button
|
||||
title="AI Assistant"
|
||||
aria-label="Ouvrir l'assistant IA"
|
||||
onClick={() => { actions.setAiOpen(!state.aiOpen); actions.setInfoOpen(false) }}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
|
||||
'p-1.5 rounded-full border transition-all duration-300',
|
||||
state.aiOpen
|
||||
? 'bg-foreground text-background border-foreground'
|
||||
: 'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
<span>AI Assistant</span>
|
||||
</button>
|
||||
|
||||
{/* Info — rounded-full */}
|
||||
<button
|
||||
aria-label="Informations du document"
|
||||
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
|
||||
state.infoOpen
|
||||
? 'bg-foreground text-background border-foreground'
|
||||
: 'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<Info size={16} />
|
||||
<span>Document Info</span>
|
||||
</button>
|
||||
|
||||
{/* Save button */}
|
||||
{/* Save — icon only */}
|
||||
{!readOnly && (
|
||||
<button
|
||||
title={state.isDirty ? 'Enregistrer' : 'Aucune modification'}
|
||||
aria-label={state.isDirty ? 'Enregistrer la note' : 'Aucune modification à enregistrer'}
|
||||
onClick={actions.handleSaveInPlace}
|
||||
disabled={state.isSaving || !state.isDirty}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
|
||||
'p-1.5 rounded-full border transition-all duration-300',
|
||||
state.isDirty
|
||||
? 'bg-foreground text-background border-foreground hover:opacity-80'
|
||||
: 'border-black/20 dark:border-white/20 text-foreground/40 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{state.isSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||
<span>{state.isSaving ? 'Saving…' : 'Save'}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -148,7 +200,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteNote(note.id)
|
||||
triggerRefresh()
|
||||
refreshNotes(note.notebookId)
|
||||
toast.success('Note supprimée.')
|
||||
onClose()
|
||||
} catch { toast.error('Impossible de supprimer.') }
|
||||
@@ -161,6 +213,20 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Info panel toggle — rightmost, icon only */}
|
||||
<button
|
||||
aria-label="Informations du document"
|
||||
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
|
||||
className={cn(
|
||||
'p-1.5 rounded-full border transition-all duration-300',
|
||||
state.infoOpen
|
||||
? 'bg-foreground text-background border-foreground'
|
||||
: 'border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
<PanelRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -277,7 +343,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
try {
|
||||
await leaveSharedNote(note.id)
|
||||
toast.success(t('notes.leftShare') || 'Share removed')
|
||||
triggerRefresh()
|
||||
refreshNotes(note.notebookId)
|
||||
onClose()
|
||||
} catch {
|
||||
toast.error(t('general.error'))
|
||||
|
||||
@@ -29,8 +29,8 @@ export function NoteTitleBlock() {
|
||||
}}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
'w-full text-4xl md:text-5xl font-memento-serif font-bold border-0 outline-none px-0 bg-transparent text-foreground leading-tight',
|
||||
'placeholder:text-foreground/20 resize-none overflow-hidden',
|
||||
'w-full text-4xl md:text-5xl font-memento-serif font-bold border-0 outline-none px-0 bg-transparent text-foreground leading-tight break-words',
|
||||
'placeholder:text-foreground/20 resize-none overflow-hidden break-words',
|
||||
!readOnly && 'pr-12'
|
||||
)}
|
||||
/>
|
||||
@@ -39,21 +39,26 @@ export function NoteTitleBlock() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
console.log('[TITLE] Sparkles button clicked')
|
||||
const plain = state.content.replace(/<[^>]+>/g, ' ').trim()
|
||||
const wordCount = plain.split(/\s+/).filter(Boolean).length
|
||||
console.log('[TITLE] Content length:', plain.length, 'Word count:', wordCount)
|
||||
if (wordCount < 10) {
|
||||
toast.error('Ajoutez au moins 10 mots avant de générer un titre.')
|
||||
return
|
||||
}
|
||||
actions.setIsProcessingAI(true)
|
||||
try {
|
||||
console.log('[TITLE] Calling /api/ai/title-suggestions...')
|
||||
const res = await fetch('/api/ai/title-suggestions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: plain }),
|
||||
})
|
||||
console.log('[TITLE] API response:', res.status)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
console.log('[TITLE] Suggestions:', data.suggestions)
|
||||
const s = data.suggestions?.[0]?.title ?? ''
|
||||
if (s) {
|
||||
actions.setTitle(s)
|
||||
@@ -62,9 +67,14 @@ export function NoteTitleBlock() {
|
||||
toast.error('Impossible de générer un titre.')
|
||||
}
|
||||
} else {
|
||||
const err = await res.text()
|
||||
console.error('[TITLE] API error:', err)
|
||||
toast.error('Erreur lors de la génération du titre.')
|
||||
}
|
||||
} catch { toast.error('Erreur réseau.') } finally { actions.setIsProcessingAI(false) }
|
||||
} catch (e) {
|
||||
console.error('[TITLE] Fetch failed:', e)
|
||||
toast.error('Erreur réseau.')
|
||||
} finally { actions.setIsProcessingAI(false) }
|
||||
}}
|
||||
disabled={state.isProcessingAI}
|
||||
className="absolute right-0 top-2 opacity-0 group-hover:opacity-60 hover:!opacity-100 transition-opacity rounded-lg p-2 text-foreground/50 hover:bg-black/5"
|
||||
|
||||
Reference in New Issue
Block a user