feat: image AI titles (3 suggestions), describe-images action, pin/list fixes, i18n
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s
- Add image description service + API route for AI-powered image analysis - Image title generation returns 3 selectable suggestions via TitleSuggestions component - Add "Describe images" action in AI assistant (individual + collective) - Fix pin refresh propagation in card and tabs view - Fix note creation refresh in tabs mode, pass all notes to tabs view - Add RTL support (dir="auto") on note content elements - Pass UI language dynamically to AI endpoints instead of hardcoded 'fr' - Add 18 missing i18n keys in both en.json and fr.json - Sparkles button on images for AI title generation (bottom-right, pulse animation) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,7 @@ interface NotesTabsViewProps {
|
||||
noteHistoryMode?: 'manual' | 'auto'
|
||||
onOpenHistory?: (note: Note) => void
|
||||
onEnableHistory?: (noteId: string) => Promise<void>
|
||||
onNoteCreated?: (note: Note) => void
|
||||
}
|
||||
|
||||
type SortOrder = 'date-desc' | 'date-asc' | 'title-asc' | 'title-desc'
|
||||
@@ -256,6 +257,7 @@ function SortableNoteListItem({
|
||||
|
||||
{/* Row 2: title */}
|
||||
<p
|
||||
dir="auto"
|
||||
className={cn(
|
||||
'mb-1.5 text-[13.5px] leading-snug transition-colors',
|
||||
selected
|
||||
@@ -268,7 +270,7 @@ function SortableNoteListItem({
|
||||
|
||||
{/* Row 3: snippet */}
|
||||
{snippet && (
|
||||
<p className="line-clamp-2 text-[12px] leading-relaxed text-muted-foreground/60">
|
||||
<p dir="auto" className="line-clamp-2 text-[12px] leading-relaxed text-muted-foreground/60">
|
||||
{snippet}
|
||||
</p>
|
||||
)}
|
||||
@@ -560,7 +562,6 @@ function NoteMetaSidebar({
|
||||
icon={note.isPinned ? <PinOff className="h-3.5 w-3.5" /> : <Pin className="h-3.5 w-3.5" />}
|
||||
label={note.isPinned ? t('notes.unpin') : t('notes.pin')}
|
||||
onClick={() => onPinToggle(note)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
{/* Archive */}
|
||||
@@ -593,10 +594,11 @@ export function NotesTabsView({
|
||||
notes,
|
||||
onEdit,
|
||||
currentNotebookId,
|
||||
|
||||
|
||||
noteHistoryMode = 'manual',
|
||||
onOpenHistory,
|
||||
onEnableHistory,
|
||||
onNoteCreated,
|
||||
}: NotesTabsViewProps) {
|
||||
const { t, language } = useLanguage()
|
||||
const { triggerRefresh } = useNoteRefreshOptional()
|
||||
@@ -667,21 +669,18 @@ export function NotesTabsView({
|
||||
return () => window.removeEventListener('label-deleted', handler)
|
||||
}, [])
|
||||
|
||||
// Sorted display items (does NOT affect persisted order)
|
||||
// Sorted display items — pinned notes always float to the top
|
||||
const sortedItems = useMemo(() => {
|
||||
if (sortOrder === 'date-desc') return [...items]
|
||||
return [...items].sort((a, b) => {
|
||||
if (sortOrder === 'date-asc') {
|
||||
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||
}
|
||||
if (sortOrder === 'title-asc') {
|
||||
return (a.title || '').localeCompare(b.title || '')
|
||||
}
|
||||
if (sortOrder === 'title-desc') {
|
||||
return (b.title || '').localeCompare(a.title || '')
|
||||
}
|
||||
const pinned = items.filter(n => n.isPinned)
|
||||
const unpinned = items.filter(n => !n.isPinned)
|
||||
const sortFn = (a: Note, b: Note) => {
|
||||
if (sortOrder === 'date-desc') return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
if (sortOrder === 'date-asc') return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||
if (sortOrder === 'title-asc') return (a.title || '').localeCompare(b.title || '')
|
||||
if (sortOrder === 'title-desc') return (b.title || '').localeCompare(a.title || '')
|
||||
return 0
|
||||
})
|
||||
}
|
||||
return [...pinned.sort(sortFn), ...unpinned.sort(sortFn)]
|
||||
}, [items, sortOrder])
|
||||
|
||||
const sensors = useSensors(
|
||||
@@ -727,6 +726,7 @@ export function NotesTabsView({
|
||||
return [...pinned, newNote, ...unpinned]
|
||||
})
|
||||
setSelectedId(newNote.id)
|
||||
onNoteCreated?.(newNote)
|
||||
triggerRefresh()
|
||||
} catch {
|
||||
toast.error(t('notes.createFailed') || 'Impossible de créer la note')
|
||||
@@ -739,6 +739,7 @@ export function NotesTabsView({
|
||||
setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: next } : n))
|
||||
try {
|
||||
await updateNote(note.id, { isPinned: next }, { skipRevalidation: true })
|
||||
triggerRefresh()
|
||||
toast.success(next ? (t('notes.pinned') || 'Épinglée') : (t('notes.unpinned') || 'Désépinglée'))
|
||||
} catch {
|
||||
setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: note.isPinned } : n))
|
||||
|
||||
Reference in New Issue
Block a user