fix(graph): resolution des bugs du graphe globale, support RTL, dates localisees et simulation D3 ultra-stable
Some checks failed
CI / Lint, Test & Build (push) Failing after 53s
CI / Deploy production (on server) (push) Has been skipped

This commit is contained in:
Antigravity
2026-05-24 19:02:54 +00:00
parent e881004c77
commit 7a8307f4b4
2 changed files with 56 additions and 24 deletions

View File

@@ -127,7 +127,7 @@ export async function GET(request: NextRequest) {
const title = (noteA.title ?? '').trim().toLowerCase()
if (title.length < 3) continue
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const re = new RegExp(`\\b${escaped}\\b`, 'i')
const re = new RegExp(`(?<!\\p{L})${escaped}(?!\\p{L})`, 'ui')
for (const noteB of notes) {
if (noteA.id === noteB.id) continue
if (re.test(stripHtml(noteB.content))) upsertEdge(noteA.id, noteB.id, 1.0, 'title_mention')

View File

@@ -67,6 +67,7 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
const { notebooks } = useNotebooks()
const containerRef = useRef<HTMLDivElement>(null)
const graphRef = useRef<any>(null)
const existingNodesRef = useRef<Map<string, any>>(new Map())
const [dimensions, setDimensions] = useState({ width: 800, height: 600 })
const [rawData, setRawData] = useState<RawData | null>(null)
const [loading, setLoading] = useState(true)
@@ -81,7 +82,7 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
const [focusNodeId, setFocusNodeId] = useState<string | null>(null)
const [controlsOpen, setControlsOpen] = useState(!embedded)
const { t } = useLanguage()
const { t, language } = useLanguage()
const plainText = useCallback((html: string | null | undefined) =>
(html ?? '')
@@ -114,6 +115,19 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
return plainText(notePreview.content).length
}, [notePreview, plainText])
const isRtl = useMemo(() => {
if (!notePreview?.content) return false
const sample = plainText(notePreview.content).replace(/\s+/g, '').slice(0, 400)
const rtlChars = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/
let rtl = 0
let ltr = 0
for (const ch of sample) {
if (rtlChars.test(ch)) rtl++
else if (/[A-Za-z]/.test(ch)) ltr++
}
return rtl > 0 && rtl >= ltr
}, [notePreview, plainText])
// ─── Resize ───────────────────────────────────────────────────────────────
useEffect(() => {
const el = containerRef.current
@@ -210,14 +224,27 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
})
return {
nodes: filtered.map(n => ({
id: n.id,
name: n.title,
val: 1 + Math.min(n.degree, 8) * 0.5,
color: colorMap.get(n.notebookId) ?? '#94a3b8',
notebookId: n.notebookId,
degree: n.degree,
})),
nodes: filtered.map(n => {
const existing = existingNodesRef.current.get(n.id)
if (existing) {
existing.name = n.title
existing.val = 1 + Math.min(n.degree, 8) * 0.5
existing.color = colorMap.get(n.notebookId) ?? '#94a3b8'
existing.notebookId = n.notebookId
existing.degree = n.degree
return existing
}
const newNode = {
id: n.id,
name: n.title,
val: 1 + Math.min(n.degree, 8) * 0.5,
color: colorMap.get(n.notebookId) ?? '#94a3b8',
notebookId: n.notebookId,
degree: n.degree,
}
existingNodesRef.current.set(n.id, newNode)
return newNode
}),
links: visibleEdges.map(e => {
let color = '#cbd5e1'
let width = 2.5
@@ -420,24 +447,25 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
onRenderFramePre={paintClusters}
nodeCanvasObjectMode={() => 'after'}
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const n = node as any
if (globalScale < 0.7) return
const name: string = node.name ?? ''
const name: string = n.name ?? ''
const label = name.length > 20 ? name.slice(0, 18) + '…' : name
const fontSize = 11 / globalScale
if (fontSize > 18) return
ctx.font = `${fontSize}px -apple-system, sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
const r = Math.sqrt(node.val ?? 1) * 5
const r = Math.sqrt(n.val ?? 1) * 5
// White background behind label
const tw = ctx.measureText(label).width
const lx = node.x - tw / 2 - 2
const ly = node.y + r + 2
const lx = n.x - tw / 2 - 2
const ly = n.y + r + 2
ctx.fillStyle = 'rgba(250,250,249,0.85)'
ctx.fillRect(lx, ly, tw + 4, fontSize + 2)
// Label text
ctx.fillStyle = '#334155'
ctx.fillText(label, node.x, ly + 1)
ctx.fillText(label, n.x, ly + 1)
}}
cooldownTicks={80}
d3AlphaDecay={0.03}
@@ -597,7 +625,10 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
{selectedNotebookName}
</button>
)}
<h2 className="text-sm font-semibold text-slate-800 dark:text-slate-100 leading-snug tracking-tight select-all">
<h2
dir={isRtl ? 'rtl' : 'ltr'}
className={`text-sm font-semibold text-slate-800 dark:text-slate-100 leading-snug tracking-tight select-all ${isRtl ? 'text-right font-persian font-semibold' : 'text-left font-sans'}`}
>
{selectedNode.title || <span className="italic text-concrete/40">{t('notes.untitled')}</span>}
</h2>
</div>
@@ -613,8 +644,8 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
<div className="px-5 py-3.5 bg-slate-50/50 dark:bg-stone-950/20 border-b border-border/30 grid grid-cols-2 gap-y-2 gap-x-4 text-[10px] text-concrete/60 select-none">
<div className="flex items-center gap-1.5">
<Calendar size={11} className="text-concrete/40 shrink-0" />
<span className="truncate" title={new Date(selectedNode.createdAt).toLocaleString()}>
{new Date(selectedNode.createdAt).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })}
<span className="truncate" title={new Date(selectedNode.createdAt).toLocaleString(language === 'fa' ? 'fa-IR' : language)}>
{new Date(selectedNode.createdAt).toLocaleDateString(language === 'fa' ? 'fa-IR' : language, { day: '2-digit', month: 'short', year: 'numeric' })}
</span>
</div>
<div className="flex items-center gap-1.5">
@@ -634,7 +665,7 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
<Clock size={11} className="text-concrete/40 shrink-0" />
<span className="truncate">
{t('graphView.preview.updated')}{' '}
{new Date(notePreview.updatedAt).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}
{new Date(notePreview.updatedAt).toLocaleDateString(language === 'fa' ? 'fa-IR' : language, { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}
</span>
</div>
)}
@@ -667,22 +698,23 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
<>
{/* Note Content Renderer */}
{notePreview.type === 'checklist' && notePreview.checkItems && notePreview.checkItems.length > 0 ? (
<div className="space-y-2 select-none">
<div className="space-y-2 select-none" dir={isRtl ? 'rtl' : 'ltr'}>
<NoteChecklist
items={notePreview.checkItems}
onToggleItem={() => {}}
/>
</div>
) : notePreview.type === 'markdown' || notePreview.isMarkdown ? (
<div className="text-xs text-slate-600 dark:text-stone-300">
<div className="text-xs text-slate-600 dark:text-stone-300" dir={isRtl ? 'rtl' : 'ltr'}>
<MarkdownContent
content={notePreview.content}
className="prose-h1:text-sm prose-h1:font-bold prose-h1:text-slate-800 dark:prose-h1:text-stone-100 prose-h1:mt-3 prose-h1:mb-1 prose-h2:text-xs prose-h2:font-bold prose-h2:text-slate-700 dark:prose-h2:text-stone-200 prose-h2:mt-2 prose-h2:mb-1 prose-p:text-xs prose-p:leading-relaxed prose-p:mb-2 prose-ul:list-disc prose-ul:pl-4 prose-ol:list-decimal prose-ol:pl-4"
className={`prose-h1:text-sm prose-h1:font-bold prose-h1:text-slate-800 dark:prose-h1:text-stone-100 prose-h1:mt-3 prose-h1:mb-1 prose-h2:text-xs prose-h2:font-bold prose-h2:text-slate-700 dark:prose-h2:text-stone-200 prose-h2:mt-2 prose-h2:mb-1 prose-p:text-xs prose-p:leading-relaxed prose-p:mb-2 prose-ul:list-disc prose-ul:pl-4 prose-ol:list-decimal prose-ol:pl-4 ${isRtl ? 'text-right font-persian' : 'text-left font-sans'}`}
/>
</div>
) : (
<div
className="text-xs text-slate-600 dark:text-stone-300 space-y-2 leading-relaxed break-words
dir={isRtl ? 'rtl' : 'ltr'}
className={`text-xs text-slate-600 dark:text-stone-300 space-y-2 leading-relaxed break-words ${isRtl ? 'text-right font-persian' : 'text-left font-sans'}
[&_h1]:text-sm [&_h1]:font-bold [&_h1]:text-slate-800 dark:[&_h1]:text-stone-100 [&_h1]:mt-4 [&_h1]:mb-1.5 [&_h1]:first:mt-0
[&_h2]:text-xs [&_h2]:font-bold [&_h2]:text-slate-700 dark:[&_h2]:text-stone-200 [&_h2]:mt-3 [&_h2]:mb-1 [&_h2]:first:mt-0
[&_h3]:text-xs [&_h3]:font-semibold [&_h3]:text-slate-600 dark:[&_h3]:text-stone-300 [&_h3]:mt-2 [&_h3]:mb-1 [&_h3]:first:mt-0
@@ -695,7 +727,7 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
[&_code]:px-1 [&_code]:py-0.5 [&_code]:bg-slate-100 dark:[&_code]:bg-stone-850 [&_code]:rounded [&_code]:font-mono [&_code]:text-[10px]
[&_pre]:p-2.5 [&_pre]:bg-slate-900 [&_pre]:text-slate-100 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_pre]:font-mono [&_pre]:text-[10px] [&_pre]:my-2
[&_blockquote]:border-l-2 [&_blockquote]:border-slate-300 dark:[&_blockquote]:border-stone-700 [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-slate-500 [&_blockquote]:my-2
[&_a]:text-indigo-600 dark:[&_a]:text-indigo-400 [&_a]:underline [&_a]:hover:text-indigo-500"
[&_a]:text-indigo-600 dark:[&_a]:text-indigo-400 [&_a]:underline [&_a]:hover:text-indigo-500`}
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
)}