- 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
128 lines
5.3 KiB
TypeScript
128 lines
5.3 KiB
TypeScript
'use client'
|
|
|
|
import { useNoteEditorContext } from './note-editor-context'
|
|
import { TitleSuggestions } from '@/components/title-suggestions'
|
|
import { Loader2, Sparkles } from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { cn } from '@/lib/utils'
|
|
import { toast } from 'sonner'
|
|
|
|
export function NoteTitleBlock() {
|
|
const { state, actions, readOnly, fullPage } = useNoteEditorContext()
|
|
const { t } = useLanguage()
|
|
|
|
if (fullPage) {
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Title — auto-resizing textarea to prevent overflow */}
|
|
<div className="group relative">
|
|
<textarea
|
|
dir="auto"
|
|
rows={1}
|
|
placeholder={t('notes.titlePlaceholder') || 'Untitled…'}
|
|
value={state.title}
|
|
onChange={(e) => { actions.setTitle(e.target.value) }}
|
|
onInput={(e) => {
|
|
const el = e.currentTarget
|
|
el.style.height = 'auto'
|
|
el.style.height = el.scrollHeight + 'px'
|
|
}}
|
|
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 break-words',
|
|
'placeholder:text-foreground/20 resize-none overflow-hidden break-words',
|
|
!readOnly && 'pr-12'
|
|
)}
|
|
/>
|
|
{/* AI title generation — always visible on hover */}
|
|
{!readOnly && (
|
|
<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)
|
|
toast.success('Titre généré !')
|
|
} else {
|
|
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 (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"
|
|
title="Générer un titre automatique avec l'IA"
|
|
>
|
|
{state.isProcessingAI ? <Loader2 className="h-5 w-5 animate-spin" /> : <Sparkles className="h-5 w-5" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Auto title suggestions */}
|
|
{!state.title && !state.dismissedTitleSuggestions && state.titleSuggestions.length > 0 && (
|
|
<TitleSuggestions
|
|
suggestions={state.titleSuggestions}
|
|
onSelect={(s: string) => { actions.setTitle(s); actions.setDismissedTitleSuggestions(true) }}
|
|
onDismiss={() => actions.setDismissedTitleSuggestions(true)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Dialog mode title block
|
|
return (
|
|
<div className="relative">
|
|
<input
|
|
dir="auto"
|
|
placeholder={t('notes.titlePlaceholder')}
|
|
value={state.title}
|
|
onChange={(e) => actions.setTitle(e.target.value)}
|
|
disabled={readOnly}
|
|
className={cn(
|
|
"w-full text-lg font-semibold border-0 focus-visible:ring-0 px-0 bg-transparent pr-10",
|
|
readOnly && "cursor-default"
|
|
)}
|
|
/>
|
|
<button
|
|
onClick={actions.handleGenerateTitles}
|
|
disabled={state.isGeneratingTitles || readOnly}
|
|
className="absolute right-0 top-1/2 -translate-y-1/2 p-1 hover:bg-purple-100 dark:hover:bg-purple-900 rounded transition-colors"
|
|
title={state.isGeneratingTitles ? t('ai.titleGenerating') : t('ai.titleGenerateWithAI')}
|
|
>
|
|
{state.isGeneratingTitles ? (
|
|
<div className="w-4 h-4 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
|
) : (
|
|
<Sparkles className="w-4 h-4 text-purple-600 hover:text-purple-700 dark:text-purple-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
)
|
|
} |