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:
178
memento-note/components/linked-note-preview-dialog.tsx
Normal file
178
memento-note/components/linked-note-preview-dialog.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||
import { ExternalLink, Link2, Columns2, GitMerge, Loader2, X } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface LinkedNotePreviewDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
noteId: string
|
||||
initialTitle?: string | null
|
||||
initialExcerpt?: string
|
||||
onInsertCitation?: () => void
|
||||
onCompare?: () => void
|
||||
onMerge?: () => void
|
||||
citationLoading?: boolean
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function openNoteInNewTab(noteId: string) {
|
||||
window.open(`/home?openNote=${encodeURIComponent(noteId)}`, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
const linkActionClass =
|
||||
'inline-flex items-center gap-1 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors hover:underline'
|
||||
|
||||
export function LinkedNotePreviewDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
noteId,
|
||||
initialTitle,
|
||||
initialExcerpt,
|
||||
onInsertCitation,
|
||||
onCompare,
|
||||
onMerge,
|
||||
citationLoading = false,
|
||||
}: LinkedNotePreviewDialogProps) {
|
||||
const { t } = useLanguage()
|
||||
const [title, setTitle] = useState(initialTitle ?? '')
|
||||
const [content, setContent] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadError, setLoadError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !noteId) return
|
||||
let cancelled = false
|
||||
setIsLoading(true)
|
||||
setLoadError(false)
|
||||
setTitle(initialTitle ?? '')
|
||||
setContent('')
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/notes/${noteId}`)
|
||||
if (!res.ok) throw new Error('fetch failed')
|
||||
const data = await res.json()
|
||||
if (cancelled) return
|
||||
if (data.success && data.data) {
|
||||
setTitle(data.data.title || t('memoryEcho.comparison.untitled'))
|
||||
setContent(data.data.content || '')
|
||||
} else {
|
||||
setLoadError(true)
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setLoadError(true)
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isOpen, noteId, initialTitle, t])
|
||||
|
||||
const displayTitle = title || initialTitle || t('memoryEcho.comparison.untitled')
|
||||
const plainBody = content ? stripHtml(content) : (initialExcerpt ?? '')
|
||||
const isHtml = content.includes('<')
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={open => { if (!open) onClose() }}>
|
||||
<DialogContent showCloseButton={false} className="max-w-lg max-h-[72vh] overflow-hidden flex flex-col p-0 gap-0">
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-border/50">
|
||||
<DialogTitle className="text-sm font-semibold truncate pr-2 leading-snug">
|
||||
{displayTitle}
|
||||
</DialogTitle>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-0.5 rounded text-muted-foreground hover:text-foreground shrink-0"
|
||||
aria-label={t('memoryEcho.editorSection.close')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3 min-h-0">
|
||||
{isLoading && !plainBody && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
{t('memoryEcho.editorSection.loading')}
|
||||
</div>
|
||||
)}
|
||||
{loadError && !plainBody && (
|
||||
<p className="text-xs text-muted-foreground">{t('memoryEcho.preview.loadError')}</p>
|
||||
)}
|
||||
{content && isHtml && (
|
||||
<div
|
||||
className={cn(
|
||||
'prose prose-sm dark:prose-invert max-w-none',
|
||||
'prose-p:text-sm prose-p:leading-relaxed prose-p:my-2',
|
||||
'prose-headings:text-sm prose-headings:font-semibold prose-headings:mt-3 prose-headings:mb-1',
|
||||
'prose-li:text-sm prose-table:text-xs'
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
)}
|
||||
{!isHtml && plainBody && (
|
||||
<div className="text-sm leading-relaxed text-foreground/90 whitespace-pre-wrap font-serif">
|
||||
{plainBody}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2.5 border-t border-border/50 flex items-center justify-between gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openNoteInNewTab(noteId)}
|
||||
className={linkActionClass}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{t('memoryEcho.editorSection.openInEditor')}
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{onCompare && (
|
||||
<button
|
||||
type="button"
|
||||
className={linkActionClass}
|
||||
onClick={() => { onCompare(); onClose() }}
|
||||
>
|
||||
<Columns2 className="h-3 w-3" />
|
||||
{t('memoryEcho.editorSection.compare')}
|
||||
</button>
|
||||
)}
|
||||
{onInsertCitation && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={citationLoading}
|
||||
className="inline-flex items-center gap-1 text-[11px] font-bold text-indigo-600 dark:text-indigo-400 hover:underline disabled:opacity-50"
|
||||
onClick={() => { onInsertCitation(); onClose() }}
|
||||
>
|
||||
<Link2 className="h-3 w-3" />
|
||||
{citationLoading
|
||||
? t('memoryEcho.editorSection.embedding')
|
||||
: t('memoryEcho.editorSection.embedPassage')}
|
||||
</button>
|
||||
)}
|
||||
{onMerge && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-[11px] font-semibold text-purple-600 dark:text-purple-400 hover:underline"
|
||||
onClick={() => { onMerge(); onClose() }}
|
||||
>
|
||||
<GitMerge className="h-3 w-3" />
|
||||
{t('memoryEcho.editorSection.merge')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user