fix(graph): resolution des bugs du graphe globale, support RTL, dates localisees et simulation D3 ultra-stable
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user