Adi12345 commited on
Commit
18a1756
·
verified ·
1 Parent(s): 3941281

Create graph_canvas_patch.py

Browse files
Files changed (1) hide show
  1. dvnc_ai_v2_hf/graph_canvas_patch.py +977 -0
dvnc_ai_v2_hf/graph_canvas_patch.py ADDED
@@ -0,0 +1,977 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import json
3
+ import re
4
+ from typing import Any, Dict, List
5
+
6
+
7
+ TYPE_STYLES = {
8
+ "topic": {"color": "#38bdf8", "size": 12, "label": "Topic"},
9
+ "query": {"color": "#38bdf8", "size": 12, "label": "Topic"},
10
+ "paper": {"color": "#34d399", "size": 9, "label": "Paper"},
11
+ "uploadedpdf": {"color": "#f59e0b", "size": 10, "label": "Uploaded PDF"},
12
+ "upload": {"color": "#f59e0b", "size": 10, "label": "Uploaded PDF"},
13
+ "concept": {"color": "#a78bfa", "size": 7, "label": "Concept"},
14
+ "author": {"color": "#f472b6", "size": 6, "label": "Author"},
15
+ "reference": {"color": "#94a3b8", "size": 6, "label": "Reference"},
16
+ "claim": {"color": "#fb7185", "size": 7, "label": "Claim"},
17
+ "frontierpaper": {"color": "#fde047", "size": 8, "label": "Frontier Paper"},
18
+ "frontier": {"color": "#fde047", "size": 8, "label": "Frontier Paper"},
19
+ }
20
+
21
+
22
+ def _safe_text(x: Any, default: str = "") -> str:
23
+ return html.escape(str(x if x is not None else default))
24
+
25
+
26
+ def _norm_text(x: Any) -> str:
27
+ return re.sub(r"\s+", " ", str(x or "")).strip()
28
+
29
+
30
+ def _truncate(text: str, limit: int = 120) -> str:
31
+ text = _norm_text(text)
32
+ return text if len(text) <= limit else text[: limit - 1].rstrip() + "…"
33
+
34
+
35
+ def _as_dict(payload: Any) -> Dict[str, Any]:
36
+ if payload is None:
37
+ return {"status": "empty", "nodes": [], "edges": []}
38
+ if isinstance(payload, dict):
39
+ return payload
40
+ if isinstance(payload, str):
41
+ try:
42
+ parsed = json.loads(payload)
43
+ if isinstance(parsed, dict):
44
+ return parsed
45
+ except Exception:
46
+ pass
47
+ return {"status": "empty", "nodes": [], "edges": []}
48
+
49
+
50
+ def _style_for(kind: str) -> Dict[str, Any]:
51
+ key = _norm_text(kind).lower().replace(" ", "")
52
+ return TYPE_STYLES.get(key, {"color": "#cbd5e1", "size": 6, "label": _norm_text(kind or "Node") or "Node"})
53
+
54
+
55
+ def _normalize_payload(payload: Any, title: str) -> Dict[str, Any]:
56
+ raw = _as_dict(payload)
57
+ raw_nodes = raw.get("nodes", []) or []
58
+ raw_edges = raw.get("edges", raw.get("links", [])) or []
59
+
60
+ nodes: List[Dict[str, Any]] = []
61
+ seen = set()
62
+
63
+ for i, node in enumerate(raw_nodes):
64
+ if not isinstance(node, dict):
65
+ continue
66
+ node_id = _norm_text(node.get("id") or f"node_{i}")
67
+ if not node_id or node_id in seen:
68
+ continue
69
+ seen.add(node_id)
70
+
71
+ kind = _norm_text(node.get("kind") or node.get("type") or "paper")
72
+ style = _style_for(kind)
73
+ label = _truncate(node.get("label") or node.get("title") or node_id, 120)
74
+
75
+ nodes.append({
76
+ "id": node_id,
77
+ "label": label,
78
+ "kind": kind or "paper",
79
+ "color": style["color"],
80
+ "size": float(node.get("size") or style["size"]),
81
+ "detail": {
82
+ "type": style["label"],
83
+ "label": label,
84
+ "title": _norm_text(node.get("title") or node.get("label") or node_id),
85
+ "venue": _norm_text(node.get("venue")),
86
+ "year": _norm_text(node.get("year")),
87
+ "doi": _norm_text(node.get("doi")),
88
+ "source": _norm_text(node.get("source")),
89
+ "authors_text": _norm_text(node.get("authors_text")),
90
+ "text": _norm_text(node.get("text")),
91
+ },
92
+ })
93
+
94
+ links: List[Dict[str, Any]] = []
95
+ for edge in raw_edges:
96
+ if not isinstance(edge, dict):
97
+ continue
98
+ src = _norm_text(edge.get("source"))
99
+ tgt = _norm_text(edge.get("target"))
100
+ if not src or not tgt or src == tgt:
101
+ continue
102
+ links.append({
103
+ "source": src,
104
+ "target": tgt,
105
+ "type": _norm_text(edge.get("type") or "RELATES_TO"),
106
+ })
107
+
108
+ counts: Dict[str, int] = {}
109
+ for n in nodes:
110
+ label = _style_for(n["kind"])["label"]
111
+ counts[label] = counts.get(label, 0) + 1
112
+
113
+ return {
114
+ "title": _norm_text(title) or "Self-Learning Knowledge Graph",
115
+ "nodes": nodes,
116
+ "links": links,
117
+ "counts": counts,
118
+ }
119
+
120
+
121
+ def render_graph_canvas_html(payload: Any, title: str = "Self-Learning Knowledge Graph", height: int = 780) -> str:
122
+ data = _normalize_payload(payload, title=title)
123
+ payload_json = json.dumps(data, ensure_ascii=False).replace("</script>", "<\\/script>")
124
+
125
+ html_doc = r"""
126
+ <!doctype html>
127
+ <html>
128
+ <head>
129
+ <meta charset="utf-8"/>
130
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
131
+ <style>
132
+ :root {
133
+ --bg0: #030712;
134
+ --bg1: #0b1220;
135
+ --bg2: #111827;
136
+ --panel: rgba(8, 15, 29, 0.72);
137
+ --panel-border: rgba(255,255,255,0.10);
138
+ --text: #e5eefc;
139
+ --muted: #a5b4cc;
140
+ --accent: #38bdf8;
141
+ }
142
+ * { box-sizing: border-box; }
143
+ html, body {
144
+ margin: 0;
145
+ width: 100%;
146
+ height: 100%;
147
+ overflow: hidden;
148
+ background:
149
+ radial-gradient(circle at 20% 20%, rgba(56,189,248,0.16), transparent 28%),
150
+ radial-gradient(circle at 80% 18%, rgba(167,139,250,0.12), transparent 24%),
151
+ linear-gradient(180deg, var(--bg2) 0%, var(--bg1) 45%, var(--bg0) 100%);
152
+ color: var(--text);
153
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
154
+ touch-action: none;
155
+ }
156
+ #app {
157
+ position: relative;
158
+ width: 100%;
159
+ height: 100%;
160
+ overflow: hidden;
161
+ }
162
+ #canvas {
163
+ position: absolute;
164
+ inset: 0;
165
+ width: 100%;
166
+ height: 100%;
167
+ display: block;
168
+ cursor: grab;
169
+ }
170
+ #canvas.dragging { cursor: grabbing; }
171
+
172
+ .hud {
173
+ position: absolute;
174
+ z-index: 10;
175
+ backdrop-filter: blur(10px);
176
+ background: var(--panel);
177
+ border: 1px solid var(--panel-border);
178
+ box-shadow: 0 10px 30px rgba(0,0,0,0.28);
179
+ }
180
+ .topbar {
181
+ top: 16px;
182
+ left: 16px;
183
+ width: min(420px, calc(100% - 32px));
184
+ border-radius: 16px;
185
+ padding: 14px 16px;
186
+ }
187
+ .topbar h3 {
188
+ margin: 0 0 6px;
189
+ font-size: 18px;
190
+ line-height: 1.2;
191
+ color: #f8fbff;
192
+ }
193
+ .topbar p {
194
+ margin: 0;
195
+ font-size: 12px;
196
+ line-height: 1.45;
197
+ color: var(--muted);
198
+ }
199
+ .legend {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ gap: 8px;
203
+ margin-top: 10px;
204
+ }
205
+ .chip {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ gap: 8px;
209
+ padding: 6px 10px;
210
+ border-radius: 999px;
211
+ font-size: 11px;
212
+ line-height: 1;
213
+ color: #dbeafe;
214
+ background: rgba(255,255,255,0.04);
215
+ border: 1px solid rgba(255,255,255,0.06);
216
+ }
217
+ .dot {
218
+ width: 10px;
219
+ height: 10px;
220
+ border-radius: 999px;
221
+ flex: 0 0 10px;
222
+ }
223
+
224
+ .details {
225
+ right: 16px;
226
+ bottom: 16px;
227
+ width: min(360px, calc(100% - 32px));
228
+ max-height: min(48%, 360px);
229
+ overflow: auto;
230
+ border-radius: 16px;
231
+ padding: 14px 16px;
232
+ }
233
+ .details h4 {
234
+ margin: 0 0 8px;
235
+ font-size: 14px;
236
+ color: #f8fbff;
237
+ }
238
+ .details p {
239
+ margin: 0;
240
+ font-size: 12px;
241
+ line-height: 1.5;
242
+ color: var(--muted);
243
+ }
244
+ .details dl {
245
+ margin: 12px 0 0;
246
+ display: grid;
247
+ grid-template-columns: 78px 1fr;
248
+ gap: 8px 10px;
249
+ font-size: 12px;
250
+ }
251
+ .details dt {
252
+ color: #93c5fd;
253
+ font-weight: 600;
254
+ }
255
+ .details dd {
256
+ margin: 0;
257
+ color: #e5eefc;
258
+ word-break: break-word;
259
+ }
260
+
261
+ .statusbar {
262
+ position: absolute;
263
+ left: 16px;
264
+ bottom: 16px;
265
+ z-index: 10;
266
+ display: flex;
267
+ gap: 8px;
268
+ flex-wrap: wrap;
269
+ }
270
+ .status {
271
+ padding: 8px 10px;
272
+ border-radius: 12px;
273
+ font-size: 11px;
274
+ color: #dbeafe;
275
+ background: rgba(8, 15, 29, 0.66);
276
+ border: 1px solid rgba(255,255,255,0.08);
277
+ backdrop-filter: blur(8px);
278
+ }
279
+
280
+ .empty {
281
+ position: absolute;
282
+ inset: 0;
283
+ display: none;
284
+ align-items: center;
285
+ justify-content: center;
286
+ z-index: 5;
287
+ pointer-events: none;
288
+ }
289
+ .empty.show { display: flex; }
290
+ .empty-card {
291
+ width: min(520px, calc(100% - 32px));
292
+ text-align: center;
293
+ padding: 24px 28px;
294
+ border-radius: 20px;
295
+ background: rgba(8, 15, 29, 0.66);
296
+ border: 1px solid rgba(255,255,255,0.08);
297
+ backdrop-filter: blur(10px);
298
+ box-shadow: 0 16px 40px rgba(0,0,0,0.34);
299
+ }
300
+ .empty-card h3 {
301
+ margin: 0 0 10px;
302
+ font-size: 22px;
303
+ }
304
+ .empty-card p {
305
+ margin: 0;
306
+ color: var(--muted);
307
+ line-height: 1.55;
308
+ }
309
+
310
+ @media (max-width: 700px) {
311
+ .topbar {
312
+ width: calc(100% - 20px);
313
+ left: 10px;
314
+ top: 10px;
315
+ padding: 12px;
316
+ }
317
+ .details {
318
+ width: calc(100% - 20px);
319
+ right: 10px;
320
+ bottom: 10px;
321
+ max-height: 34%;
322
+ padding: 12px;
323
+ }
324
+ .statusbar {
325
+ left: 10px;
326
+ bottom: calc(34% + 22px);
327
+ right: 10px;
328
+ }
329
+ }
330
+ </style>
331
+ </head>
332
+ <body>
333
+ <div id="app">
334
+ <canvas id="canvas"></canvas>
335
+
336
+ <div class="hud topbar">
337
+ <h3 id="title"></h3>
338
+ <p>Drag background to rotate, mouse wheel or trackpad pinch to zoom, two-finger pinch on touch screens, Shift-drag or right-drag to pan, and drag a node to move it in 3D space.</p>
339
+ <div class="legend" id="legend"></div>
340
+ </div>
341
+
342
+ <div class="hud details" id="details">
343
+ <h4>Node details</h4>
344
+ <p>Tap or click a node to inspect it.</p>
345
+ </div>
346
+
347
+ <div class="statusbar" id="statusbar"></div>
348
+
349
+ <div class="empty" id="empty">
350
+ <div class="empty-card">
351
+ <h3>No graph data yet</h3>
352
+ <p>Ingest papers or pass a payload containing nodes and edges. This renderer accepts the existing graph payload shape directly.</p>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <script>
358
+ const DATA = __DATA__;
359
+
360
+ const canvas = document.getElementById("canvas");
361
+ const ctx = canvas.getContext("2d", { alpha: true });
362
+ const titleEl = document.getElementById("title");
363
+ const detailsEl = document.getElementById("details");
364
+ const legendEl = document.getElementById("legend");
365
+ const statusbarEl = document.getElementById("statusbar");
366
+ const emptyEl = document.getElementById("empty");
367
+
368
+ titleEl.textContent = DATA.title || "Self-Learning Knowledge Graph";
369
+
370
+ const legendMap = [
371
+ ["Topic", "#38bdf8"],
372
+ ["Paper", "#34d399"],
373
+ ["Uploaded PDF", "#f59e0b"],
374
+ ["Concept", "#a78bfa"],
375
+ ["Author", "#f472b6"],
376
+ ["Reference", "#94a3b8"],
377
+ ["Claim", "#fb7185"],
378
+ ["Frontier Paper", "#fde047"]
379
+ ];
380
+
381
+ legendMap.forEach(([label, color]) => {
382
+ const chip = document.createElement("div");
383
+ chip.className = "chip";
384
+ chip.innerHTML = `<span class="dot" style="background:${color}"></span>${label}`;
385
+ legendEl.appendChild(chip);
386
+ });
387
+
388
+ function addStatus(text) {
389
+ const el = document.createElement("div");
390
+ el.className = "status";
391
+ el.textContent = text;
392
+ statusbarEl.appendChild(el);
393
+ }
394
+
395
+ addStatus(`Nodes: ${DATA.nodes.length}`);
396
+ addStatus(`Edges: ${DATA.links.length}`);
397
+
398
+ Object.entries(DATA.counts || {}).forEach(([k, v]) => addStatus(`${k}: ${v}`));
399
+
400
+ if (!DATA.nodes.length) {
401
+ emptyEl.classList.add("show");
402
+ }
403
+
404
+ function hashCode(str) {
405
+ let h = 0;
406
+ for (let i = 0; i < str.length; i++) {
407
+ h = ((h << 5) - h) + str.charCodeAt(i);
408
+ h |= 0;
409
+ }
410
+ return Math.abs(h);
411
+ }
412
+
413
+ function seededFloat(key, salt) {
414
+ const h = hashCode(key + "|" + salt);
415
+ return (h % 10000) / 10000;
416
+ }
417
+
418
+ const nodes = DATA.nodes.map((n, i) => {
419
+ const r1 = seededFloat(n.id, "x");
420
+ const r2 = seededFloat(n.id, "y");
421
+ const r3 = seededFloat(n.id, "z");
422
+ const radius = 120 + (i % 7) * 18 + (n.size || 6) * 2;
423
+ const theta = r1 * Math.PI * 2;
424
+ const phi = (r2 - 0.5) * Math.PI;
425
+
426
+ return {
427
+ ...n,
428
+ x: Math.cos(theta) * Math.cos(phi) * radius,
429
+ y: Math.sin(phi) * radius * 0.72,
430
+ z: Math.sin(theta) * Math.cos(phi) * radius,
431
+ vx: 0,
432
+ vy: 0,
433
+ vz: 0,
434
+ fx: null,
435
+ fy: null,
436
+ fz: null,
437
+ screenX: 0,
438
+ screenY: 0,
439
+ screenR: 0,
440
+ depth: 0
441
+ };
442
+ });
443
+
444
+ const nodeById = new Map(nodes.map(n => [n.id, n]));
445
+
446
+ const links = DATA.links
447
+ .map(l => ({
448
+ ...l,
449
+ sourceRef: nodeById.get(l.source),
450
+ targetRef: nodeById.get(l.target)
451
+ }))
452
+ .filter(l => l.sourceRef && l.targetRef);
453
+
454
+ const state = {
455
+ width: 0,
456
+ height: 0,
457
+ dpr: Math.max(1, Math.min(2, window.devicePixelRatio || 1)),
458
+ cx: 0,
459
+ cy: 0,
460
+ yaw: 0.65,
461
+ pitch: 0.25,
462
+ zoom: 1.0,
463
+ panX: 0,
464
+ panY: 0,
465
+ perspective: 940,
466
+ cameraZ: 860,
467
+ hovered: null,
468
+ selected: null,
469
+ draggingNode: null,
470
+ draggingBackground: false,
471
+ panMode: false,
472
+ lastX: 0,
473
+ lastY: 0,
474
+ raf: null,
475
+ lastTouchDist: 0,
476
+ lastTouchCenter: null
477
+ };
478
+
479
+ function resize() {
480
+ const rect = canvas.getBoundingClientRect();
481
+ state.width = rect.width;
482
+ state.height = rect.height;
483
+ state.cx = rect.width / 2;
484
+ state.cy = rect.height / 2;
485
+ canvas.width = Math.floor(rect.width * state.dpr);
486
+ canvas.height = Math.floor(rect.height * state.dpr);
487
+ ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
488
+ }
489
+ resize();
490
+ window.addEventListener("resize", resize);
491
+
492
+ function clamp(v, lo, hi) {
493
+ return Math.max(lo, Math.min(hi, v));
494
+ }
495
+
496
+ function rotatePoint(x, y, z) {
497
+ const cy = Math.cos(state.yaw);
498
+ const sy = Math.sin(state.yaw);
499
+ const cp = Math.cos(state.pitch);
500
+ const sp = Math.sin(state.pitch);
501
+
502
+ const x1 = x * cy - z * sy;
503
+ const z1 = x * sy + z * cy;
504
+
505
+ const y2 = y * cp - z1 * sp;
506
+ const z2 = y * sp + z1 * cp;
507
+
508
+ return { x: x1, y: y2, z: z2 };
509
+ }
510
+
511
+ function projectNode(n) {
512
+ const p = rotatePoint(n.x, n.y, n.z);
513
+ const depth = state.cameraZ - p.z;
514
+ const scale = (state.perspective / Math.max(120, depth)) * state.zoom;
515
+ n.depth = p.z;
516
+ n.screenX = state.cx + state.panX + p.x * scale;
517
+ n.screenY = state.cy + state.panY + p.y * scale;
518
+ n.screenR = clamp((n.size || 6) * scale * 0.85, 4, 22);
519
+ n._scale = scale;
520
+ }
521
+
522
+ function setDetails(n) {
523
+ if (!n) {
524
+ detailsEl.innerHTML = `
525
+ <h4>Node details</h4>
526
+ <p>Tap or click a node to inspect it.</p>
527
+ `;
528
+ return;
529
+ }
530
+ const d = n.detail || {};
531
+ detailsEl.innerHTML = `
532
+ <h4>Node details</h4>
533
+ <dl>
534
+ <dt>Label</dt><dd>${escapeHtml(n.label || "")}</dd>
535
+ <dt>Type</dt><dd>${escapeHtml(d.type || n.kind || "")}</dd>
536
+ <dt>Title</dt><dd>${escapeHtml(d.title || "—")}</dd>
537
+ <dt>Venue</dt><dd>${escapeHtml(d.venue || "—")}</dd>
538
+ <dt>Year</dt><dd>${escapeHtml(d.year || "—")}</dd>
539
+ <dt>DOI</dt><dd>${escapeHtml(d.doi || "—")}</dd>
540
+ <dt>Source</dt><dd>${escapeHtml(d.source || "—")}</dd>
541
+ <dt>Authors</dt><dd>${escapeHtml(d.authors_text || "—")}</dd>
542
+ <dt>Text</dt><dd>${escapeHtml(d.text || "—")}</dd>
543
+ </dl>
544
+ `;
545
+ }
546
+
547
+ function escapeHtml(str) {
548
+ return String(str || "")
549
+ .replaceAll("&", "&amp;")
550
+ .replaceAll("<", "&lt;")
551
+ .replaceAll(">", "&gt;")
552
+ .replaceAll('"', "&quot;")
553
+ .replaceAll("'", "&#039;");
554
+ }
555
+
556
+ function findNodeAt(x, y) {
557
+ const ordered = [...nodes].sort((a, b) => b.depth - a.depth);
558
+ for (const n of ordered) {
559
+ const dx = x - n.screenX;
560
+ const dy = y - n.screenY;
561
+ const rr = n.screenR + 6;
562
+ if (dx * dx + dy * dy <= rr * rr) {
563
+ return n;
564
+ }
565
+ }
566
+ return null;
567
+ }
568
+
569
+ function worldDeltaFromScreen(dx, dy, scale) {
570
+ const localX = dx / Math.max(0.2, scale);
571
+ const localY = dy / Math.max(0.2, scale);
572
+
573
+ const cp = Math.cos(-state.pitch);
574
+ const sp = Math.sin(-state.pitch);
575
+ const cy = Math.cos(-state.yaw);
576
+ const sy = Math.sin(-state.yaw);
577
+
578
+ let x1 = localX;
579
+ let y1 = localY * cp;
580
+ let z1 = localY * sp;
581
+
582
+ const wx = x1 * cy - z1 * sy;
583
+ const wz = x1 * sy + z1 * cy;
584
+
585
+ return { x: wx, y: y1, z: wz };
586
+ }
587
+
588
+ function stepPhysics() {
589
+ if (!nodes.length) return;
590
+
591
+ const nCount = nodes.length;
592
+ const pairStride = nCount > 120 ? 2 : 1;
593
+ const repulsion = 15000;
594
+ const spring = 0.0035;
595
+ const centering = 0.0008;
596
+ const damping = 0.90;
597
+
598
+ for (const n of nodes) {
599
+ n.ax = 0;
600
+ n.ay = 0;
601
+ n.az = 0;
602
+ }
603
+
604
+ for (let i = 0; i < nCount; i += pairStride) {
605
+ const a = nodes[i];
606
+ for (let j = i + 1; j < nCount; j += pairStride) {
607
+ const b = nodes[j];
608
+ const dx = a.x - b.x;
609
+ const dy = a.y - b.y;
610
+ const dz = a.z - b.z;
611
+ const d2 = dx * dx + dy * dy + dz * dz + 0.01;
612
+ const f = repulsion / d2;
613
+ const inv = 1 / Math.sqrt(d2);
614
+ const fx = dx * inv * f;
615
+ const fy = dy * inv * f;
616
+ const fz = dz * inv * f;
617
+ a.ax += fx; a.ay += fy; a.az += fz;
618
+ b.ax -= fx; b.ay -= fy; b.az -= fz;
619
+ }
620
+ }
621
+
622
+ for (const l of links) {
623
+ const a = l.sourceRef;
624
+ const b = l.targetRef;
625
+ const dx = b.x - a.x;
626
+ const dy = b.y - a.y;
627
+ const dz = b.z - a.z;
628
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
629
+ const target = 80 + ((a.size || 6) + (b.size || 6)) * 3;
630
+ const f = (dist - target) * spring;
631
+ const fx = (dx / dist) * f;
632
+ const fy = (dy / dist) * f;
633
+ const fz = (dz / dist) * f;
634
+ a.ax += fx; a.ay += fy; a.az += fz;
635
+ b.ax -= fx; b.ay -= fy; b.az -= fz;
636
+ }
637
+
638
+ for (const n of nodes) {
639
+ if (state.draggingNode === n) {
640
+ n.vx = 0; n.vy = 0; n.vz = 0;
641
+ continue;
642
+ }
643
+ n.ax += -n.x * centering;
644
+ n.ay += -n.y * centering;
645
+ n.az += -n.z * centering;
646
+
647
+ n.vx = (n.vx + n.ax) * damping;
648
+ n.vy = (n.vy + n.ay) * damping;
649
+ n.vz = (n.vz + n.az) * damping;
650
+
651
+ n.x += n.vx;
652
+ n.y += n.vy;
653
+ n.z += n.vz;
654
+ }
655
+ }
656
+
657
+ function drawBackground() {
658
+ ctx.clearRect(0, 0, state.width, state.height);
659
+
660
+ const g = ctx.createRadialGradient(state.width * 0.5, state.height * 0.46, 40, state.width * 0.5, state.height * 0.46, state.width * 0.72);
661
+ g.addColorStop(0, "rgba(56,189,248,0.05)");
662
+ g.addColorStop(0.45, "rgba(99,102,241,0.03)");
663
+ g.addColorStop(1, "rgba(0,0,0,0)");
664
+ ctx.fillStyle = g;
665
+ ctx.fillRect(0, 0, state.width, state.height);
666
+ }
667
+
668
+ function drawEdge(a, b, type) {
669
+ const depthAlpha = clamp((a._scale + b._scale) * 0.22, 0.10, 0.45);
670
+ const typeColor = {
671
+ "ABOUT": "rgba(56,189,248,ALPHA)",
672
+ "MENTIONS": "rgba(167,139,250,ALPHA)",
673
+ "WRITTEN_BY": "rgba(244,114,182,ALPHA)",
674
+ "CITES": "rgba(148,163,184,ALPHA)",
675
+ "ASSERTS": "rgba(251,113,133,ALPHA)",
676
+ "FRONTIER": "rgba(253,224,71,ALPHA)",
677
+ "FRONTIER_CANDIDATE": "rgba(253,224,71,ALPHA)",
678
+ "UPLOADED_SOURCE": "rgba(245,158,11,ALPHA)"
679
+ };
680
+ const c = (typeColor[type] || "rgba(203,213,225,ALPHA)").replace("ALPHA", depthAlpha.toFixed(3));
681
+
682
+ ctx.beginPath();
683
+ ctx.moveTo(a.screenX, a.screenY);
684
+ ctx.lineTo(b.screenX, b.screenY);
685
+ ctx.lineWidth = type === "ABOUT" || type === "UPLOADED_SOURCE" ? 2 : 1.2;
686
+ ctx.strokeStyle = c;
687
+ ctx.stroke();
688
+ }
689
+
690
+ function drawNode(n, selected, hovered) {
691
+ const r = n.screenR;
692
+ ctx.beginPath();
693
+ ctx.arc(n.screenX, n.screenY, r + 8, 0, Math.PI * 2);
694
+ const glow = ctx.createRadialGradient(n.screenX, n.screenY, 0, n.screenX, n.screenY, r + 8);
695
+ glow.addColorStop(0, hexToRgba(n.color, hovered || selected ? 0.28 : 0.18));
696
+ glow.addColorStop(1, hexToRgba(n.color, 0));
697
+ ctx.fillStyle = glow;
698
+ ctx.fill();
699
+
700
+ ctx.beginPath();
701
+ ctx.arc(n.screenX, n.screenY, r, 0, Math.PI * 2);
702
+ ctx.fillStyle = n.color;
703
+ ctx.fill();
704
+
705
+ ctx.lineWidth = selected ? 2.6 : hovered ? 2.0 : 1.0;
706
+ ctx.strokeStyle = selected ? "rgba(255,255,255,0.95)" : hovered ? "rgba(255,255,255,0.75)" : "rgba(255,255,255,0.22)";
707
+ ctx.stroke();
708
+
709
+ if (selected || hovered || r > 10) {
710
+ drawLabel(n, selected || hovered);
711
+ }
712
+ }
713
+
714
+ function drawLabel(n, strong) {
715
+ const text = n.label || n.id;
716
+ ctx.font = `${strong ? 700 : 600} 12px Inter, sans-serif`;
717
+ const padX = 8;
718
+ const h = 24;
719
+ const w = Math.min(260, ctx.measureText(text).width + padX * 2);
720
+ const x = n.screenX + 10;
721
+ const y = n.screenY - 10 - h;
722
+
723
+ roundRect(x, y, w, h, 10);
724
+ ctx.fillStyle = strong ? "rgba(8,15,29,0.92)" : "rgba(8,15,29,0.72)";
725
+ ctx.fill();
726
+
727
+ ctx.lineWidth = 1;
728
+ ctx.strokeStyle = "rgba(255,255,255,0.08)";
729
+ ctx.stroke();
730
+
731
+ ctx.fillStyle = "#f8fbff";
732
+ ctx.fillText(text.length > 32 ? text.slice(0, 31) + "…" : text, x + padX, y + 16);
733
+ }
734
+
735
+ function roundRect(x, y, w, h, r) {
736
+ ctx.beginPath();
737
+ ctx.moveTo(x + r, y);
738
+ ctx.arcTo(x + w, y, x + w, y + h, r);
739
+ ctx.arcTo(x + w, y + h, x, y + h, r);
740
+ ctx.arcTo(x, y + h, x, y, r);
741
+ ctx.arcTo(x, y, x + w, y, r);
742
+ ctx.closePath();
743
+ }
744
+
745
+ function hexToRgba(hex, alpha) {
746
+ const h = hex.replace("#", "");
747
+ const bigint = parseInt(h.length === 3 ? h.split("").map(c => c + c).join("") : h, 16);
748
+ const r = (bigint >> 16) & 255;
749
+ const g = (bigint >> 8) & 255;
750
+ const b = bigint & 255;
751
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
752
+ }
753
+
754
+ function render() {
755
+ drawBackground();
756
+
757
+ for (const n of nodes) {
758
+ projectNode(n);
759
+ }
760
+
761
+ const orderedLinks = [...links].sort((a, b) => {
762
+ const da = (a.sourceRef.depth + a.targetRef.depth) * 0.5;
763
+ const db = (b.sourceRef.depth + b.targetRef.depth) * 0.5;
764
+ return da - db;
765
+ });
766
+
767
+ for (const l of orderedLinks) {
768
+ drawEdge(l.sourceRef, l.targetRef, l.type);
769
+ }
770
+
771
+ const orderedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
772
+ for (const n of orderedNodes) {
773
+ drawNode(n, state.selected === n, state.hovered === n);
774
+ }
775
+ }
776
+
777
+ function animate() {
778
+ stepPhysics();
779
+ render();
780
+ state.raf = requestAnimationFrame(animate);
781
+ }
782
+ animate();
783
+
784
+ function pointerPos(evt) {
785
+ const rect = canvas.getBoundingClientRect();
786
+ return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
787
+ }
788
+
789
+ canvas.addEventListener("contextmenu", (e) => e.preventDefault());
790
+
791
+ canvas.addEventListener("mousedown", (e) => {
792
+ const p = pointerPos(e);
793
+ state.lastX = p.x;
794
+ state.lastY = p.y;
795
+ state.hovered = findNodeAt(p.x, p.y);
796
+ state.panMode = e.button === 2 || e.shiftKey;
797
+
798
+ if (state.hovered && e.button !== 2) {
799
+ state.draggingNode = state.hovered;
800
+ state.selected = state.hovered;
801
+ setDetails(state.selected);
802
+ } else {
803
+ state.draggingBackground = true;
804
+ }
805
+
806
+ canvas.classList.add("dragging");
807
+ });
808
+
809
+ window.addEventListener("mousemove", (e) => {
810
+ const rect = canvas.getBoundingClientRect();
811
+ const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
812
+ if (!inside && !state.draggingNode && !state.draggingBackground) return;
813
+
814
+ const p = pointerPos(e);
815
+ const dx = p.x - state.lastX;
816
+ const dy = p.y - state.lastY;
817
+ state.lastX = p.x;
818
+ state.lastY = p.y;
819
+
820
+ if (state.draggingNode) {
821
+ const delta = worldDeltaFromScreen(dx, dy, state.draggingNode._scale || 1);
822
+ state.draggingNode.x += delta.x * 1.18;
823
+ state.draggingNode.y += delta.y * 1.18;
824
+ state.draggingNode.z += delta.z * 1.18;
825
+ state.draggingNode.vx = 0;
826
+ state.draggingNode.vy = 0;
827
+ state.draggingNode.vz = 0;
828
+ return;
829
+ }
830
+
831
+ if (state.draggingBackground) {
832
+ if (state.panMode) {
833
+ state.panX += dx;
834
+ state.panY += dy;
835
+ } else {
836
+ state.yaw += dx * 0.0085;
837
+ state.pitch = clamp(state.pitch + dy * 0.0065, -1.25, 1.25);
838
+ }
839
+ return;
840
+ }
841
+
842
+ state.hovered = findNodeAt(p.x, p.y);
843
+ });
844
+
845
+ window.addEventListener("mouseup", () => {
846
+ state.draggingNode = null;
847
+ state.draggingBackground = false;
848
+ state.panMode = false;
849
+ canvas.classList.remove("dragging");
850
+ });
851
+
852
+ canvas.addEventListener("wheel", (e) => {
853
+ e.preventDefault();
854
+ const delta = e.deltaY > 0 ? 0.92 : 1.09;
855
+ state.zoom = clamp(state.zoom * delta, 0.35, 4.0);
856
+ }, { passive: false });
857
+
858
+ canvas.addEventListener("click", (e) => {
859
+ const p = pointerPos(e);
860
+ const hit = findNodeAt(p.x, p.y);
861
+ if (hit) {
862
+ state.selected = hit;
863
+ setDetails(hit);
864
+ }
865
+ });
866
+
867
+ canvas.addEventListener("touchstart", (e) => {
868
+ e.preventDefault();
869
+ if (e.touches.length === 1) {
870
+ const t = e.touches[0];
871
+ const p = pointerPos(t);
872
+ state.lastX = p.x;
873
+ state.lastY = p.y;
874
+ state.hovered = findNodeAt(p.x, p.y);
875
+ if (state.hovered) {
876
+ state.draggingNode = state.hovered;
877
+ state.selected = state.hovered;
878
+ setDetails(state.selected);
879
+ } else {
880
+ state.draggingBackground = true;
881
+ }
882
+ } else if (e.touches.length === 2) {
883
+ state.draggingNode = null;
884
+ state.draggingBackground = false;
885
+ const a = e.touches[0];
886
+ const b = e.touches[1];
887
+ state.lastTouchDist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
888
+ state.lastTouchCenter = {
889
+ x: (a.clientX + b.clientX) * 0.5,
890
+ y: (a.clientY + b.clientY) * 0.5
891
+ };
892
+ }
893
+ }, { passive: false });
894
+
895
+ canvas.addEventListener("touchmove", (e) => {
896
+ e.preventDefault();
897
+ if (e.touches.length === 1) {
898
+ const t = e.touches[0];
899
+ const p = pointerPos(t);
900
+ const dx = p.x - state.lastX;
901
+ const dy = p.y - state.lastY;
902
+ state.lastX = p.x;
903
+ state.lastY = p.y;
904
+
905
+ if (state.draggingNode) {
906
+ const delta = worldDeltaFromScreen(dx, dy, state.draggingNode._scale || 1);
907
+ state.draggingNode.x += delta.x * 1.18;
908
+ state.draggingNode.y += delta.y * 1.18;
909
+ state.draggingNode.z += delta.z * 1.18;
910
+ state.draggingNode.vx = 0;
911
+ state.draggingNode.vy = 0;
912
+ state.draggingNode.vz = 0;
913
+ } else if (state.draggingBackground) {
914
+ state.yaw += dx * 0.0085;
915
+ state.pitch = clamp(state.pitch + dy * 0.0065, -1.25, 1.25);
916
+ }
917
+ } else if (e.touches.length === 2) {
918
+ const a = e.touches[0];
919
+ const b = e.touches[1];
920
+ const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
921
+ const center = {
922
+ x: (a.clientX + b.clientX) * 0.5,
923
+ y: (a.clientY + b.clientY) * 0.5
924
+ };
925
+
926
+ if (state.lastTouchDist) {
927
+ const ratio = dist / state.lastTouchDist;
928
+ state.zoom = clamp(state.zoom * ratio, 0.35, 4.0);
929
+ }
930
+
931
+ if (state.lastTouchCenter) {
932
+ state.panX += center.x - state.lastTouchCenter.x;
933
+ state.panY += center.y - state.lastTouchCenter.y;
934
+ }
935
+
936
+ state.lastTouchDist = dist;
937
+ state.lastTouchCenter = center;
938
+ }
939
+ }, { passive: false });
940
+
941
+ canvas.addEventListener("touchend", (e) => {
942
+ e.preventDefault();
943
+ if (e.touches.length === 0) {
944
+ state.draggingNode = null;
945
+ state.draggingBackground = false;
946
+ state.lastTouchDist = 0;
947
+ state.lastTouchCenter = null;
948
+ }
949
+ }, { passive: false });
950
+
951
+ setDetails(null);
952
+ </script>
953
+ </body>
954
+ </html>
955
+ """
956
+
957
+ html_doc = html_doc.replace("__DATA__", payload_json)
958
+ return (
959
+ f'<iframe title="{_safe_text(title)}" '
960
+ f'style="width:100%; height:{int(height)}px; border:0; border-radius:18px; overflow:hidden; background:#030712;" '
961
+ f'sandbox="allow-scripts allow-same-origin" '
962
+ f'srcdoc="{html.escape(html_doc, quote=True)}"></iframe>'
963
+ )
964
+
965
+
966
+ def render_graph_canvas_component(payload: Any, title: str = "Self-Learning Knowledge Graph", height: int = 780):
967
+ try:
968
+ import gradio as gr
969
+ except Exception as e:
970
+ raise RuntimeError(f"gradio is required for render_graph_canvas_component: {e}")
971
+ return gr.HTML(render_graph_canvas_html(payload, title=title, height=height))
972
+
973
+
974
+ __all__ = [
975
+ "render_graph_canvas_html",
976
+ "render_graph_canvas_component",
977
+ ]