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:
@@ -7,16 +7,17 @@ import { fr } from 'date-fns/locale/fr'
|
||||
import { enUS } from 'date-fns/locale/en-US'
|
||||
import { faIR } from 'date-fns/locale/fa-IR'
|
||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||
import { X, Info, Clock, Hash, Book, FileText, Calendar, Tag, ChevronRight, Trash2, RotateCcw, Loader2, Check, History as HistoryIcon } from 'lucide-react'
|
||||
import { X, Info, Clock, Hash, Book, FileText, Calendar, Tag, ChevronRight, Trash2, RotateCcw, Loader2, Check, History as HistoryIcon, Network, Copy } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { LabelBadge } from './label-badge'
|
||||
import { NoteHistoryModal } from './note-history-modal'
|
||||
import { NoteNetworkTab } from './note-network-tab'
|
||||
import { enableNoteHistory, commitNoteHistory, getNoteHistory, deleteNoteHistoryEntry, restoreNoteVersion } from '@/app/actions/notes'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type Tab = 'info' | 'versions'
|
||||
type Tab = 'info' | 'versions' | 'network'
|
||||
|
||||
interface NoteDocumentInfoPanelProps {
|
||||
note: Note
|
||||
@@ -39,6 +40,23 @@ function charCount(text: string) {
|
||||
return text.replace(/<[^>]+>/g, '').length
|
||||
}
|
||||
|
||||
function lineCount(text: string) {
|
||||
const plain = text.replace(/<[^>]+>/g, '\n')
|
||||
return plain.trim() ? plain.split('\n').length : 0
|
||||
}
|
||||
|
||||
function equationCount(text: string) {
|
||||
const block = (text.match(/\$\$[\s\S]+?\$\$/g) || []).length
|
||||
const inline = (text.match(/\$[^$\n]+?\$/g) || []).length
|
||||
return block + inline
|
||||
}
|
||||
|
||||
function imageCount(text: string) {
|
||||
const md = (text.match(/!\[[^\]]*\]\([^)]+\)/g) || []).length
|
||||
const html = (text.match(/<img\s/gi) || []).length
|
||||
return md + html
|
||||
}
|
||||
|
||||
export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }: NoteDocumentInfoPanelProps) {
|
||||
const { t, language } = useLanguage()
|
||||
const { notebooks } = useNotebooks()
|
||||
@@ -51,6 +69,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState<string | null>(null)
|
||||
const [isRestoring, setIsRestoring] = useState<string | null>(null)
|
||||
const [copiedId, setCopiedId] = useState(false)
|
||||
const locale = getLocale(language)
|
||||
|
||||
const displayNoteType = useMemo(() => {
|
||||
@@ -114,6 +133,9 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
|
||||
const words = useMemo(() => wordCount(content), [content])
|
||||
const chars = useMemo(() => charCount(content), [content])
|
||||
const lines = useMemo(() => lineCount(content), [content])
|
||||
const equations = useMemo(() => equationCount(content), [content])
|
||||
const images = useMemo(() => imageCount(content), [content])
|
||||
|
||||
const createdAt = note.createdAt ? new Date(note.createdAt as unknown as string) : null
|
||||
const updatedAt = note.contentUpdatedAt ? new Date(note.contentUpdatedAt as unknown as string) : null
|
||||
@@ -125,7 +147,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
{/* Header tabs */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-border/40">
|
||||
<div className="flex gap-1">
|
||||
{(['info', 'versions'] as Tab[]).map(tab => (
|
||||
{(['info', 'versions', 'network'] as Tab[]).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
@@ -138,7 +160,12 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
>
|
||||
{tab === 'info' && <Info className="h-3 w-3" />}
|
||||
{tab === 'versions' && <Clock className="h-3 w-3" />}
|
||||
{tab === 'info' ? t('documentInfo.tabInfo') : t('documentInfo.tabVersions')}
|
||||
{tab === 'network' && <Network className="h-3 w-3" />}
|
||||
{tab === 'info'
|
||||
? t('documentInfo.tabInfo')
|
||||
: tab === 'versions'
|
||||
? t('documentInfo.tabVersions')
|
||||
: t('documentInfo.tabNetwork')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -168,6 +195,19 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 border-b border-border/30 divide-x divide-border/30">
|
||||
{[
|
||||
{ value: lines, label: t('documentInfo.linesLabel') },
|
||||
{ value: equations, label: t('documentInfo.equationsLabel') },
|
||||
{ value: images, label: t('documentInfo.imagesLabel') },
|
||||
].map(({ value, label }) => (
|
||||
<div key={label} className="flex flex-col items-center gap-0.5 py-3">
|
||||
<span className="text-lg font-bold font-memento-serif tabular-nums">{value}</span>
|
||||
<span className="text-[8px] uppercase tracking-widest text-muted-foreground font-semibold">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border/30">
|
||||
{notebook && (
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
@@ -229,9 +269,24 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<Hash className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.idLabel')}</p>
|
||||
<p className="text-[11px] text-muted-foreground font-mono truncate">{note.id}</p>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<p className="text-[11px] text-muted-foreground font-mono truncate">{note.id}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(note.id).then(() => {
|
||||
setCopiedId(true)
|
||||
setTimeout(() => setCopiedId(false), 2000)
|
||||
})
|
||||
}}
|
||||
className="p-1 rounded hover:bg-muted text-muted-foreground shrink-0"
|
||||
title={t('documentInfo.copyId')}
|
||||
>
|
||||
{copiedId ? <Check className="h-3 w-3 text-emerald-500" /> : <Copy className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -382,6 +437,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── NETWORK TAB ── */}
|
||||
{activeTab === 'network' && (
|
||||
<NoteNetworkTab noteId={note.id} noteTitle={note.title || ''} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user