fix: Memory Echo sans vol de scroll, badge IA sidebar, wizard langue

- Ne plus auto-scroller vers Memory Echo : pastille bas de page + reset scroll au titre
- Badges notes/carnets générés par l’IA dans la sidebar
- Wizard : langue du contenu = langue de la requête ; refresh carnet après création
- Voix : erreurs not-allowed/no-speech moins bruyantes
This commit is contained in:
Antigravity
2026-07-16 17:25:57 +00:00
parent 45297da333
commit 704fed1191
11 changed files with 672 additions and 210 deletions

View File

@@ -48,6 +48,7 @@ import { Notebook, Note } from '@/lib/types'
import { toast } from 'sonner'
import { motion, AnimatePresence } from 'motion/react'
import { getNoteDisplayTitle } from '@/lib/note-preview'
import { listAiCreatedNotebookIds, isRecentlyCreated } from '@/lib/ai-created-highlight'
import dynamic from 'next/dynamic'
const CreateNotebookDialog = dynamic(() => import('./create-notebook-dialog').then(m => ({ default: m.CreateNotebookDialog })), { ssr: false })
@@ -79,14 +80,20 @@ function NoteLink({
title,
isActive,
isPinned,
autoGenerated,
isRecent,
onClick,
}: {
title: string
isActive: boolean
isPinned?: boolean
/** Note créée par l'IA (agents, wizard, …) */
autoGenerated?: boolean
/** Créée récemment (< 48h) — point discret « nouveau » */
isRecent?: boolean
onClick: () => void
}) {
const { language } = useLanguage()
const { language, t } = useLanguage()
const slideX = language === 'fa' || language === 'ar' ? 10 : -10
return (
<motion.button
@@ -97,14 +104,38 @@ function NoteLink({
'w-full flex items-center gap-2 ps-6 pe-3 py-1.5 text-[11px] transition-all rounded-lg text-start',
isActive
? 'bg-white dark:bg-white/10 shadow-sm border border-border/50 text-foreground font-semibold'
: 'text-muted-foreground hover:text-foreground hover:bg-white/30 dark:hover:bg-white/5',
: autoGenerated
? 'text-foreground/90 hover:text-foreground hover:bg-brand-accent/5 border border-transparent'
: 'text-muted-foreground hover:text-foreground hover:bg-white/30 dark:hover:bg-white/5',
autoGenerated && !isActive && 'bg-brand-accent/[0.04]',
)}
title={autoGenerated ? (t('sidebar.aiGeneratedNote') || 'Note générée par l\'IA') : undefined}
>
<FileText
size={12}
className={cn('shrink-0', isActive ? 'text-brand-accent' : 'text-muted-foreground/70')}
/>
{autoGenerated ? (
<Sparkles
size={12}
className={cn('shrink-0', isActive ? 'text-brand-accent' : 'text-brand-accent/80')}
aria-hidden
/>
) : (
<FileText
size={12}
className={cn('shrink-0', isActive ? 'text-brand-accent' : 'text-muted-foreground/70')}
/>
)}
<span className="truncate flex-1">{title}</span>
{autoGenerated && (
<span className="shrink-0 text-[8px] font-bold uppercase tracking-wider text-brand-accent/90 bg-brand-accent/10 px-1.5 py-0.5 rounded">
{t('sidebar.aiBadge') || 'IA'}
</span>
)}
{isRecent && !autoGenerated && (
<span
className="shrink-0 w-1.5 h-1.5 rounded-full bg-brand-accent"
title={t('sidebar.recentNote') || 'Récemment créée'}
aria-label={t('sidebar.recentNote') || 'Récemment créée'}
/>
)}
{isPinned && <Pin size={10} className="text-amber-500 fill-amber-500 shrink-0" />}
</motion.button>
)
@@ -347,6 +378,7 @@ function SidebarCarnetItem({
onDelete,
onTogglePin,
isPinned,
isAiCreated,
children,
isDragging,
dragHandleProps,
@@ -358,7 +390,7 @@ function SidebarCarnetItem({
}: {
carnet: { id: string; name: string; initial: string; isPrivate?: boolean }
isActive: boolean
notes: { id: string; title: string; isPinned?: boolean }[]
notes: { id: string; title: string; isPinned?: boolean; autoGenerated?: boolean; isRecent?: boolean }[]
activeNoteId: string | null
onCarnetClick: () => void
onNoteClick: (noteId: string, carnetId: string) => void
@@ -367,6 +399,8 @@ function SidebarCarnetItem({
onDelete: () => void
onTogglePin: () => void
isPinned: boolean
/** Carnet créé via wizard IA (repère temporaire) */
isAiCreated?: boolean
children?: React.ReactNode
isDragging?: boolean
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>
@@ -436,8 +470,13 @@ function SidebarCarnetItem({
onDoubleClick={(e) => { e.stopPropagation(); onRename() }}
className={cn(
'flex-1 flex items-center gap-2.5 px-2 py-1.5 rounded-lg transition-all duration-300 group/item cursor-pointer relative overflow-hidden',
isActive ? 'bg-white dark:bg-white/10 shadow-sm border border-border/40' : 'hover:bg-white/40 dark:hover:bg-white/5'
isActive
? 'bg-white dark:bg-white/10 shadow-sm border border-border/40'
: isAiCreated
? 'hover:bg-brand-accent/10 border border-brand-accent/20 bg-brand-accent/[0.06]'
: 'hover:bg-white/40 dark:hover:bg-white/5',
)}
title={isAiCreated ? (t('sidebar.aiGeneratedNotebook') || 'Carnet créé avec l\'IA') : undefined}
>
{isActive && (
<motion.div
@@ -446,6 +485,9 @@ function SidebarCarnetItem({
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
{isAiCreated && !isActive && (
<span className="absolute -start-1 w-1 h-4 bg-brand-accent/70 rounded-full" aria-hidden />
)}
<div className={cn(
'w-5 h-5 flex items-center justify-center shrink-0 transition-colors',
isActive ? 'text-brand-accent' : 'text-muted-foreground/80',
@@ -460,6 +502,12 @@ function SidebarCarnetItem({
)}>
{carnet.name}
</span>
{isAiCreated && (
<span className="shrink-0 inline-flex items-center gap-0.5 text-[8px] font-bold uppercase tracking-wider text-brand-accent bg-brand-accent/15 px-1.5 py-0.5 rounded">
<Sparkles size={9} aria-hidden />
{t('sidebar.aiBadge') || 'IA'}
</span>
)}
{carnet.isPrivate && <Lock size={10} className="text-concrete/60 shrink-0" />}
{!isActive && hasActiveDescendant && (
<span className="w-1.5 h-1.5 rounded-full bg-brand-accent/70 shrink-0" />
@@ -573,6 +621,8 @@ function SidebarCarnetItem({
key={note.id}
title={note.title}
isPinned={note.isPinned}
autoGenerated={note.autoGenerated}
isRecent={note.isRecent}
isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id, carnet.id)}
/>
@@ -635,7 +685,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const [isRenaming, setIsRenaming] = useState(false)
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set())
const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string; isPinned?: boolean }[]>>({})
const [notebookNotes, setNotebookNotes] = useState<Record<string, { id: string; title: string; isPinned?: boolean; autoGenerated?: boolean; isRecent?: boolean }[]>>({})
const [aiNotebookIds, setAiNotebookIds] = useState<Set<string>>(() => new Set())
const [activeView, setActiveView] = useState<NavigationView>('notebooks')
const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
const [showSortMenu, setShowSortMenu] = useState(false)
@@ -886,6 +937,18 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const notebookIdsKey = useMemo(() => notebooks.map(nb => nb.id).sort().join(','), [notebooks])
// Repères carnets IA (localStorage TTL 7j)
useEffect(() => {
const refresh = () => setAiNotebookIds(listAiCreatedNotebookIds())
refresh()
window.addEventListener('memento-ai-created-changed', refresh)
window.addEventListener('storage', refresh)
return () => {
window.removeEventListener('memento-ai-created-changed', refresh)
window.removeEventListener('storage', refresh)
}
}, [])
useEffect(() => {
if (!notebookIdsKey) return
let cancelled = false
@@ -897,6 +960,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled')),
isPinned: n.isPinned,
autoGenerated: Boolean(n.autoGenerated),
isRecent: isRecentlyCreated(n.createdAt, 48),
}))
return [nb.id, mapped] as const
})
@@ -1261,6 +1326,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
onDelete={() => setDeletingNotebook(notebook)}
onTogglePin={() => togglePin(notebook.id)}
isPinned={pinnedIds.has(notebook.id)}
isAiCreated={aiNotebookIds.has(notebook.id)}
isDragging={isDragging}
level={level}
isExpanded={isExpanded}
@@ -1273,7 +1339,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</motion.div>
)
})
}, [rootNotebooks, childNotebooks, filteredNotebookIds, currentNotebookId, currentNoteId, notebookNotes, draggedId, dropTarget, dropAction, expandedIds, pinnedIds, toggleExpand, handleCarnetClick, handleNoteClick, handleDragStart, handleDragEnd, handleDropOnNotebook, handleStartRename])
}, [rootNotebooks, childNotebooks, filteredNotebookIds, currentNotebookId, currentNoteId, notebookNotes, draggedId, dropTarget, dropAction, expandedIds, pinnedIds, aiNotebookIds, toggleExpand, handleCarnetClick, handleNoteClick, handleDragStart, handleDragEnd, handleDropOnNotebook, handleStartRename])
return (
<>
@@ -1977,8 +2043,34 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{showAiWizard && (
<AiNotebookWizard
onClose={() => setShowAiWizard(false)}
onComplete={(notebookId: string) => {
onComplete={async (notebookId: string) => {
// 1) Marquer IA + expand dans la sidebar
try {
const { markAiCreatedNotebook } = await import('@/lib/ai-created-highlight')
markAiCreatedNotebook(notebookId)
} catch { /* ignore */ }
setAiNotebookIds((prev) => new Set(prev).add(notebookId))
setExpandedIds((prev) => {
const next = new Set(prev)
next.add(notebookId)
return next
})
// 2) Recharger la liste des carnets (sinon le carnet n'apparaît qu'après F5)
await refreshNotebooks()
// 3) Forcer le rechargement des notes de ce carnet dans la sidebar
try {
const notes = await getAllNotes(false, notebookId)
const mapped = notes.map((n: Note) => ({
id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled')),
isPinned: n.isPinned,
autoGenerated: Boolean(n.autoGenerated),
isRecent: true,
}))
setNotebookNotes((prev) => ({ ...prev, [notebookId]: mapped }))
} catch { /* ignore */ }
setShowAiWizard(false)
// 4) Ouvrir le carnet
router.push(`/home?notebook=${notebookId}`)
}}
/>