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() const title = (noteA.title ?? '').trim().toLowerCase()
if (title.length < 3) continue if (title.length < 3) continue
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 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) { for (const noteB of notes) {
if (noteA.id === noteB.id) continue if (noteA.id === noteB.id) continue
if (re.test(stripHtml(noteB.content))) upsertEdge(noteA.id, noteB.id, 1.0, 'title_mention') 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 { notebooks } = useNotebooks()
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const graphRef = useRef<any>(null) const graphRef = useRef<any>(null)
const existingNodesRef = useRef<Map<string, any>>(new Map())
const [dimensions, setDimensions] = useState({ width: 800, height: 600 }) const [dimensions, setDimensions] = useState({ width: 800, height: 600 })
const [rawData, setRawData] = useState<RawData | null>(null) const [rawData, setRawData] = useState<RawData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -81,7 +82,7 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
const [focusNodeId, setFocusNodeId] = useState<string | null>(null) const [focusNodeId, setFocusNodeId] = useState<string | null>(null)
const [controlsOpen, setControlsOpen] = useState(!embedded) const [controlsOpen, setControlsOpen] = useState(!embedded)
const { t } = useLanguage() const { t, language } = useLanguage()
const plainText = useCallback((html: string | null | undefined) => const plainText = useCallback((html: string | null | undefined) =>
(html ?? '') (html ?? '')
@@ -114,6 +115,19 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
return plainText(notePreview.content).length return plainText(notePreview.content).length
}, [notePreview, plainText]) }, [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 ─────────────────────────────────────────────────────────────── // ─── Resize ───────────────────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
const el = containerRef.current const el = containerRef.current
@@ -210,14 +224,27 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
}) })
return { return {
nodes: filtered.map(n => ({ nodes: filtered.map(n => {
id: n.id, const existing = existingNodesRef.current.get(n.id)
name: n.title, if (existing) {
val: 1 + Math.min(n.degree, 8) * 0.5, existing.name = n.title
color: colorMap.get(n.notebookId) ?? '#94a3b8', existing.val = 1 + Math.min(n.degree, 8) * 0.5
notebookId: n.notebookId, existing.color = colorMap.get(n.notebookId) ?? '#94a3b8'
degree: n.degree, 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 => { links: visibleEdges.map(e => {
let color = '#cbd5e1' let color = '#cbd5e1'
let width = 2.5 let width = 2.5
@@ -420,24 +447,25 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
onRenderFramePre={paintClusters} onRenderFramePre={paintClusters}
nodeCanvasObjectMode={() => 'after'} nodeCanvasObjectMode={() => 'after'}
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const n = node as any
if (globalScale < 0.7) return 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 label = name.length > 20 ? name.slice(0, 18) + '…' : name
const fontSize = 11 / globalScale const fontSize = 11 / globalScale
if (fontSize > 18) return if (fontSize > 18) return
ctx.font = `${fontSize}px -apple-system, sans-serif` ctx.font = `${fontSize}px -apple-system, sans-serif`
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.textBaseline = 'top' ctx.textBaseline = 'top'
const r = Math.sqrt(node.val ?? 1) * 5 const r = Math.sqrt(n.val ?? 1) * 5
// White background behind label // White background behind label
const tw = ctx.measureText(label).width const tw = ctx.measureText(label).width
const lx = node.x - tw / 2 - 2 const lx = n.x - tw / 2 - 2
const ly = node.y + r + 2 const ly = n.y + r + 2
ctx.fillStyle = 'rgba(250,250,249,0.85)' ctx.fillStyle = 'rgba(250,250,249,0.85)'
ctx.fillRect(lx, ly, tw + 4, fontSize + 2) ctx.fillRect(lx, ly, tw + 4, fontSize + 2)
// Label text // Label text
ctx.fillStyle = '#334155' ctx.fillStyle = '#334155'
ctx.fillText(label, node.x, ly + 1) ctx.fillText(label, n.x, ly + 1)
}} }}
cooldownTicks={80} cooldownTicks={80}
d3AlphaDecay={0.03} d3AlphaDecay={0.03}
@@ -597,7 +625,10 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
{selectedNotebookName} {selectedNotebookName}
</button> </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>} {selectedNode.title || <span className="italic text-concrete/40">{t('notes.untitled')}</span>}
</h2> </h2>
</div> </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="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"> <div className="flex items-center gap-1.5">
<Calendar size={11} className="text-concrete/40 shrink-0" /> <Calendar size={11} className="text-concrete/40 shrink-0" />
<span className="truncate" title={new Date(selectedNode.createdAt).toLocaleString()}> <span className="truncate" title={new Date(selectedNode.createdAt).toLocaleString(language === 'fa' ? 'fa-IR' : language)}>
{new Date(selectedNode.createdAt).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })} {new Date(selectedNode.createdAt).toLocaleDateString(language === 'fa' ? 'fa-IR' : language, { day: '2-digit', month: 'short', year: 'numeric' })}
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5"> <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" /> <Clock size={11} className="text-concrete/40 shrink-0" />
<span className="truncate"> <span className="truncate">
{t('graphView.preview.updated')}{' '} {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> </span>
</div> </div>
)} )}
@@ -667,22 +698,23 @@ export function NoteGraphView({ embedded = false }: { embedded?: boolean }) {
<> <>
{/* Note Content Renderer */} {/* Note Content Renderer */}
{notePreview.type === 'checklist' && notePreview.checkItems && notePreview.checkItems.length > 0 ? ( {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 <NoteChecklist
items={notePreview.checkItems} items={notePreview.checkItems}
onToggleItem={() => {}} onToggleItem={() => {}}
/> />
</div> </div>
) : notePreview.type === 'markdown' || notePreview.isMarkdown ? ( ) : 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 <MarkdownContent
content={notePreview.content} 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>
) : ( ) : (
<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 [&_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 [&_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 [&_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] [&_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 [&_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 [&_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 }} dangerouslySetInnerHTML={{ __html: htmlContent }}
/> />
)} )}