feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
145
memento-note/components/note-link-picker.tsx
Normal file
145
memento-note/components/note-link-picker.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { FileText, Loader2, Search, X } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export interface NoteLinkOption {
|
||||
id: string
|
||||
title: string | null
|
||||
notebookId: string | null
|
||||
}
|
||||
|
||||
interface NoteLinkPickerProps {
|
||||
isOpen: boolean
|
||||
query: string
|
||||
currentNoteId?: string
|
||||
onClose: () => void
|
||||
onSelect: (note: NoteLinkOption) => void
|
||||
}
|
||||
|
||||
export function NoteLinkPicker({
|
||||
isOpen,
|
||||
query,
|
||||
currentNoteId,
|
||||
onClose,
|
||||
onSelect,
|
||||
}: NoteLinkPickerProps) {
|
||||
const { t } = useLanguage()
|
||||
const [searchQuery, setSearchQuery] = useState(query)
|
||||
const [results, setResults] = useState<NoteLinkOption[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery(query)
|
||||
}, [isOpen, query])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
setLoading(true)
|
||||
const timer = setTimeout(() => {
|
||||
const params = new URLSearchParams({ limit: '15' })
|
||||
if (searchQuery.trim()) params.set('search', searchQuery.trim())
|
||||
fetch(`/api/notes?${params}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const notes: NoteLinkOption[] = (data.data || [])
|
||||
.filter((n: { id: string }) => n.id !== currentNoteId)
|
||||
.map((n: { id: string; title: string | null; notebookId: string | null }) => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
notebookId: n.notebookId,
|
||||
}))
|
||||
setResults(notes)
|
||||
})
|
||||
.catch(() => setResults([]))
|
||||
.finally(() => setLoading(false))
|
||||
}, 250)
|
||||
return () => clearTimeout(timer)
|
||||
}, [isOpen, searchQuery, currentNoteId])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/30 dark:bg-black/50 backdrop-blur-sm">
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0, y: 12 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.95, opacity: 0, y: 12 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="w-[440px] max-w-full bg-background rounded-2xl border border-border shadow-2xl flex flex-col max-h-[70vh] overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-4 border-b border-border/60 flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-2.5 min-w-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#A47148]/10 flex items-center justify-center text-[#A47148] shrink-0">
|
||||
<FileText size={16} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold">{t('richTextEditor.noteLinkPickerTitle')}</h3>
|
||||
<p className="text-[11px] text-muted-foreground leading-snug mt-0.5">
|
||||
{t('richTextEditor.noteLinkPickerHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1 rounded-full text-muted-foreground hover:bg-muted transition-colors shrink-0"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3 border-b border-border/40">
|
||||
<div className="relative">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
autoFocus
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder={t('richTextEditor.noteLinkPickerSearch')}
|
||||
className="w-full pl-9 pr-3 py-2 text-sm rounded-lg border border-border bg-muted/30 outline-none focus:ring-2 focus:ring-[#A47148]/30"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 p-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-8 text-muted-foreground text-sm">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
) : results.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 px-4 leading-relaxed">
|
||||
{t('richTextEditor.noteLinkPickerEmpty')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{results.map(note => (
|
||||
<li key={note.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(note)}
|
||||
className="w-full text-left px-3 py-2.5 rounded-xl hover:bg-muted transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium line-clamp-2">
|
||||
{note.title?.trim() || t('documentInfo.network.untitled')}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user