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:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef, type ReactNode } from 'react'
|
||||
import { ChevronDown, ChevronUp, Sparkles, Link2, X, Loader2, HelpCircle } from 'lucide-react'
|
||||
import { LinkedNotePreviewDialog } from '@/components/linked-note-preview-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -12,7 +12,6 @@ import { toast } from 'sonner'
|
||||
import { useNoteEditorContext } from '@/components/note-editor/note-editor-context'
|
||||
import { stripHtmlToPlainText } from '@/lib/text/plain-text'
|
||||
import { detectTextDirection } from '@/lib/clip/rtl-content'
|
||||
import { SEMANTIC_SIMILARITY_FLOOR_CLIP } from '@/lib/ai/semantic-proximity'
|
||||
|
||||
interface ConnectionData {
|
||||
noteId: string
|
||||
@@ -106,6 +105,9 @@ export function MemoryEchoSection({
|
||||
const [embeddingId, setEmbeddingId] = useState<string | null>(null)
|
||||
const [helpOpen, setHelpOpen] = useState(false)
|
||||
const [previewTarget, setPreviewTarget] = useState<PreviewTarget | null>(null)
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const [sectionInView, setSectionInView] = useState(false)
|
||||
const [cueDismissed, setCueDismissed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -113,6 +115,8 @@ export function MemoryEchoSection({
|
||||
setConsentRequired(false)
|
||||
setConnections([])
|
||||
setRetroRefs([])
|
||||
setCueDismissed(false)
|
||||
setSectionInView(false)
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -154,18 +158,24 @@ export function MemoryEchoSection({
|
||||
}
|
||||
}, [noteId])
|
||||
|
||||
// Scroll doux vers la section quand une forte connexion apparaît (une fois par note)
|
||||
// Observe section visibility — never auto-scroll; cue only when off-screen
|
||||
useEffect(() => {
|
||||
if (isLoading || connections.length === 0) return
|
||||
const top = connections[0]
|
||||
if (!top || top.similarity < SEMANTIC_SIMILARITY_FLOOR_CLIP) return
|
||||
const key = `memory-echo-scroll-${noteId}`
|
||||
if (sessionStorage.getItem(key)) return
|
||||
sessionStorage.setItem(key, '1')
|
||||
requestAnimationFrame(() => {
|
||||
document.getElementById('memory-echo-section')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
})
|
||||
}, [isLoading, connections, noteId])
|
||||
const el = sectionRef.current
|
||||
if (!el) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setSectionInView(entry.isIntersecting && entry.intersectionRatio > 0.08)
|
||||
},
|
||||
{ root: null, threshold: [0, 0.08, 0.2, 0.5] },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [noteId, isLoading, connections.length, retroRefs.length, consentRequired, isVisible])
|
||||
|
||||
const scrollToSection = useCallback(() => {
|
||||
sectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}, [])
|
||||
|
||||
const handleEmbed = useCallback(async (conn: ConnectionData) => {
|
||||
setEmbeddingId(conn.noteId)
|
||||
@@ -251,8 +261,29 @@ export function MemoryEchoSection({
|
||||
const topConnection = connections[0]
|
||||
const restConnections = connections.slice(1)
|
||||
|
||||
// Floating cue: signal activity below without stealing the title viewport
|
||||
const showBottomCue =
|
||||
!cueDismissed &&
|
||||
!sectionInView &&
|
||||
(isLoading || hasConnections || hasRetro || consentRequired)
|
||||
|
||||
const cueLabel = isLoading
|
||||
? t('memoryEcho.editorSection.bottomCueLoading')
|
||||
: hasConnections
|
||||
? connections.length === 1
|
||||
? t('memoryEcho.editorSection.bottomCueFoundOne')
|
||||
: t('memoryEcho.editorSection.bottomCueFound', { count: connections.length })
|
||||
: hasRetro
|
||||
? t('memoryEcho.editorSection.bottomCueRetro', { count: retroRefs.length })
|
||||
: t('memoryEcho.editorSection.bottomCueConsent')
|
||||
|
||||
return (
|
||||
<div id="memory-echo-section" className="mt-10 space-y-6 scroll-mt-24">
|
||||
<>
|
||||
<div
|
||||
id="memory-echo-section"
|
||||
ref={sectionRef}
|
||||
className="mt-10 space-y-6 scroll-mt-24"
|
||||
>
|
||||
{isLoading && (
|
||||
<div
|
||||
className="rounded-2xl border border-indigo-500/10 bg-gradient-to-br from-indigo-500/[0.03] to-transparent p-5 animate-pulse space-y-3"
|
||||
@@ -551,6 +582,47 @@ export function MemoryEchoSection({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showBottomCue && (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-6 left-1/2 z-40 -translate-x-1/2',
|
||||
'flex items-center gap-1 rounded-full border border-indigo-500/25',
|
||||
'bg-background/95 shadow-lg shadow-indigo-500/10 backdrop-blur-md',
|
||||
'pl-3 pr-1.5 py-1.5 max-w-[min(92vw,22rem)]',
|
||||
'animate-in fade-in slide-in-from-bottom-2 duration-300',
|
||||
)}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToSection}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 rounded-full px-1 py-0.5 text-left transition-colors hover:bg-indigo-500/5"
|
||||
title={t('memoryEcho.editorSection.bottomCueScroll')}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-indigo-500" />
|
||||
) : (
|
||||
<Sparkles className="h-3.5 w-3.5 shrink-0 text-indigo-500" />
|
||||
)}
|
||||
<span className="truncate text-xs font-medium text-foreground/90">
|
||||
{cueLabel}
|
||||
</span>
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-indigo-500/80 animate-bounce" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCueDismissed(true)}
|
||||
className="shrink-0 rounded-full p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label={t('memoryEcho.editorSection.bottomCueDismiss')}
|
||||
title={t('memoryEcho.editorSection.bottomCueDismiss')}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user