Files
Momento/memento-note/components/linked-note-preview-dialog.tsx
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
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>
2026-05-24 14:27:29 +00:00

179 lines
6.2 KiB
TypeScript

'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>
)
}