Ian Wu Claude Opus 4.6 commited on
Commit
34d3917
Β·
1 Parent(s): 57f9a8f

Interactive DAG viewer for TCS papers

Browse files

Static HTML app that fetches paper DAGs from the AI-Math-TCS/tcs_dags
HuggingFace dataset and visualizes them as interactive D3.js graphs.
Features: paper selector panel, tree/force layouts, node detail panel
with KaTeX math rendering, importance filters, and search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +6 -8
  2. index.html +1282 -18
  3. style.css +0 -28
README.md CHANGED
@@ -1,12 +1,10 @@
1
  ---
2
- title: Tcs Dag Viewer
3
- emoji: πŸ‘
4
- colorFrom: green
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
8
- license: apache-2.0
9
- short_description: View TCS papers as DAGs
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: TCS Paper DAG Viewer
3
+ emoji: πŸ”¬
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
+ datasets:
9
+ - AI-Math-TCS/tcs_dags
10
  ---
 
 
index.html CHANGED
@@ -1,19 +1,1283 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Paper DAG Viewer β€” HuggingFace</title>
7
+ <script src="https://d3js.org/d3.v7.min.js"></script>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
9
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
10
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"></script>
11
+ <style>
12
+ :root {
13
+ --bg: #0f1117;
14
+ --surface: #1a1d27;
15
+ --surface2: #242836;
16
+ --border: #2e3348;
17
+ --text: #e2e4ed;
18
+ --text-dim: #8b8fa3;
19
+ --accent: #7c8aff;
20
+ --accent-dim: #5c6adf;
21
+ }
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
25
+ background: var(--bg); color: var(--text);
26
+ overflow: hidden; height: 100vh;
27
+ }
28
+
29
+ /* Layout */
30
+ #app { display: flex; height: 100vh; }
31
+
32
+ /* Paper selector panel (left) */
33
+ #paper-panel {
34
+ width: 320px; background: var(--surface); border-right: 1px solid var(--border);
35
+ display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden;
36
+ transition: width 0.2s;
37
+ }
38
+ #paper-panel.collapsed { width: 0; border: none; overflow: hidden; }
39
+ #paper-panel-header {
40
+ padding: 16px; border-bottom: 1px solid var(--border); flex-shrink: 0;
41
+ }
42
+ #paper-panel-header h2 {
43
+ font-size: 13px; font-weight: 600; color: var(--text-dim);
44
+ text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px;
45
+ }
46
+ #paper-search {
47
+ width: 100%; background: var(--surface2); border: 1px solid var(--border);
48
+ border-radius: 6px; color: var(--text); padding: 7px 12px; font-size: 13px; outline: none;
49
+ }
50
+ #paper-search:focus { border-color: var(--accent); }
51
+ #paper-search::placeholder { color: var(--text-dim); }
52
+ #paper-count {
53
+ font-size: 11px; color: var(--text-dim); margin-top: 8px;
54
+ }
55
+ #paper-list {
56
+ flex: 1; overflow-y: auto; padding: 8px;
57
+ }
58
+ .paper-item {
59
+ padding: 10px 12px; border-radius: 6px; cursor: pointer;
60
+ margin-bottom: 4px; transition: background 0.15s; border: 1px solid transparent;
61
+ }
62
+ .paper-item:hover { background: var(--surface2); }
63
+ .paper-item.active { background: var(--accent-dim); border-color: var(--accent); }
64
+ .paper-item .paper-title {
65
+ font-size: 13px; font-weight: 500; line-height: 1.3;
66
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
67
+ }
68
+ .paper-item .paper-meta {
69
+ font-size: 11px; color: var(--text-dim); margin-top: 4px;
70
+ display: flex; gap: 8px;
71
+ }
72
+ .paper-item.active .paper-meta { color: rgba(255,255,255,0.7); }
73
+
74
+ /* Toggle button for paper panel */
75
+ #toggle-paper-panel {
76
+ position: absolute; left: 320px; top: 50%; z-index: 11;
77
+ background: var(--surface); border: 1px solid var(--border); border-left: none;
78
+ border-radius: 0 6px 6px 0; padding: 8px 4px; cursor: pointer;
79
+ color: var(--text-dim); font-size: 14px; transition: left 0.2s;
80
+ }
81
+ #toggle-paper-panel:hover { color: var(--text); }
82
+ #paper-panel.collapsed ~ #toggle-paper-panel { left: 0; }
83
+
84
+ /* Graph area */
85
+ #graph-container { flex: 1; position: relative; }
86
+ #detail-panel {
87
+ width: 640px; background: var(--surface); border-left: 1px solid var(--border);
88
+ overflow-y: auto; display: flex; flex-direction: column; transition: width 0.2s;
89
+ }
90
+ #detail-panel.collapsed { width: 0; border: none; }
91
+
92
+ /* Toolbar */
93
+ #toolbar {
94
+ position: absolute; top: 0; left: 0; right: 0; z-index: 10;
95
+ background: linear-gradient(to bottom, var(--bg) 60%, transparent);
96
+ padding: 16px 20px 32px; display: flex; align-items: center; gap: 12px;
97
+ }
98
+ #toolbar h1 {
99
+ font-size: 14px; font-weight: 600; color: var(--text-dim);
100
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 400px;
101
+ }
102
+ #search {
103
+ background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
104
+ color: var(--text); padding: 6px 12px; font-size: 13px; width: 200px; outline: none;
105
+ }
106
+ #search:focus { border-color: var(--accent); }
107
+ #search::placeholder { color: var(--text-dim); }
108
+ .toolbar-group { display: flex; gap: 4px; }
109
+ .filter-btn {
110
+ background: var(--surface2); border: 1px solid var(--border); border-radius: 4px;
111
+ color: var(--text-dim); padding: 4px 10px; font-size: 11px; cursor: pointer;
112
+ text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.15s;
113
+ }
114
+ .filter-btn:hover { border-color: var(--text-dim); color: var(--text); }
115
+ .filter-btn.active { background: var(--accent-dim); border-color: var(--accent); color: #fff; }
116
+ .spacer { flex: 1; }
117
+
118
+ /* Legend */
119
+ #legend {
120
+ position: absolute; bottom: 16px; left: 16px; z-index: 10;
121
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
122
+ padding: 12px 16px; font-size: 11px; display: flex; flex-direction: column; gap: 6px;
123
+ }
124
+ .legend-row { display: flex; align-items: center; gap: 8px; }
125
+ .legend-dot {
126
+ width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
127
+ }
128
+ .legend-line {
129
+ width: 24px; height: 2px; flex-shrink: 0;
130
+ }
131
+
132
+ /* Tooltip */
133
+ #tooltip {
134
+ position: absolute; pointer-events: none; z-index: 20;
135
+ background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
136
+ padding: 8px 12px; font-size: 12px; max-width: 300px;
137
+ opacity: 0; transition: opacity 0.15s;
138
+ }
139
+ #tooltip.show { opacity: 1; }
140
+ #tooltip .tt-type { color: var(--accent); font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
141
+ #tooltip .tt-title { font-weight: 600; margin: 2px 0; }
142
+ #tooltip .tt-summary { color: var(--text-dim); font-size: 11px; }
143
+
144
+ /* SVG */
145
+ svg { width: 100%; height: 100%; }
146
+ .link { fill: none; stroke-width: 1.5; }
147
+ .link.primary { stroke: #4a5080; }
148
+ .link.secondary { stroke: #3a3e55; stroke-dasharray: 5 4; }
149
+ .link.highlighted { stroke: var(--accent); stroke-width: 2.5; }
150
+ .link.secondary.highlighted { stroke: var(--accent-dim); stroke-width: 2; }
151
+ .link.faded { opacity: 0.08; }
152
+ .node-circle { cursor: pointer; stroke-width: 2; transition: r 0.15s; }
153
+ .node-circle.faded { opacity: 0.15; }
154
+ .node-circle.selected { stroke: #fff !important; stroke-width: 3; }
155
+ .node-label {
156
+ font-size: 11px; fill: var(--text-dim); pointer-events: none;
157
+ text-anchor: middle; dominant-baseline: central;
158
+ }
159
+ .node-label.faded { opacity: 0.1; }
160
+ .arrowhead { fill: #4a5080; }
161
+ .arrowhead.highlighted { fill: var(--accent); }
162
+
163
+ /* Detail panel */
164
+ .panel-header {
165
+ padding: 20px; border-bottom: 1px solid var(--border);
166
+ position: sticky; top: 0; background: var(--surface); z-index: 1;
167
+ }
168
+ .panel-header .node-type-badge {
169
+ display: inline-block; font-size: 10px; text-transform: uppercase;
170
+ letter-spacing: 0.5px; padding: 2px 8px; border-radius: 3px;
171
+ margin-bottom: 8px;
172
+ }
173
+ .panel-header h2 { font-size: 16px; font-weight: 600; line-height: 1.3; }
174
+ .panel-header .node-id { color: var(--text-dim); font-size: 12px; margin-top: 4px; }
175
+ .panel-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
176
+ .panel-section h3 {
177
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
178
+ color: var(--text-dim); margin-bottom: 6px;
179
+ }
180
+ .panel-section p, .panel-section pre {
181
+ font-size: 13px; line-height: 1.5; color: var(--text);
182
+ }
183
+ .panel-section pre {
184
+ background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
185
+ padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-word;
186
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px;
187
+ max-height: 400px; overflow-y: auto;
188
+ }
189
+ .panel-section .latex-block {
190
+ background: var(--surface2); border: 1px solid var(--border); border-radius: 6px;
191
+ padding: 14px; overflow-x: auto; overflow-y: auto; max-height: 500px;
192
+ font-size: 14px; line-height: 1.7;
193
+ }
194
+ .panel-section .latex-block .katex-display { margin: 0.6em 0; }
195
+ .panel-section .latex-block .katex { font-size: 1em; }
196
+ .panel-section .meta-grid {
197
+ display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 12px;
198
+ }
199
+ .panel-section .meta-key { color: var(--text-dim); }
200
+ .panel-section .meta-val { color: var(--text); }
201
+ .panel-section .edge-list { list-style: none; display: flex; flex-direction: column; gap: 6px; }
202
+ .panel-section .edge-item {
203
+ font-size: 12px; padding: 6px 10px; background: var(--surface2);
204
+ border-radius: 4px; cursor: pointer; transition: background 0.15s;
205
+ }
206
+ .panel-section .edge-item:hover { background: var(--border); }
207
+ .edge-item .edge-relation {
208
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px;
209
+ color: var(--accent); margin-right: 6px;
210
+ }
211
+ .edge-item .edge-primary { color: var(--text-dim); font-size: 10px; }
212
+ .panel-empty {
213
+ display: flex; align-items: center; justify-content: center;
214
+ height: 100%; color: var(--text-dim); font-size: 13px;
215
+ }
216
+ .importance-badge {
217
+ display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 3px;
218
+ margin-left: 6px;
219
+ }
220
+ .importance-badge.critical_path { background: #3d1f1f; color: #ff8a8a; }
221
+ .importance-badge.supporting { background: #1f2d3d; color: #8ac4ff; }
222
+ .importance-badge.context { background: #2d2d1f; color: #d4d48a; }
223
+
224
+ .proof-type-badge {
225
+ display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 3px;
226
+ margin-bottom: 6px;
227
+ }
228
+ .proof-type-badge.proof { background: #1f3d2a; color: #8affa8; }
229
+ .proof-type-badge.sketch { background: #3d3d1f; color: #ffd48a; }
230
+ .proof-type-badge.citation { background: #2a1f3d; color: #c48aff; }
231
+
232
+ /* Collapse button (right panel) */
233
+ #toggle-panel {
234
+ position: absolute; right: 640px; top: 50%; z-index: 11;
235
+ background: var(--surface); border: 1px solid var(--border); border-right: none;
236
+ border-radius: 6px 0 0 6px; padding: 8px 4px; cursor: pointer;
237
+ color: var(--text-dim); font-size: 14px; transition: right 0.2s;
238
+ }
239
+ #toggle-panel:hover { color: var(--text); }
240
+ #detail-panel.collapsed ~ #toggle-panel { right: 0; }
241
+
242
+ /* Loading overlay */
243
+ #loading {
244
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
245
+ background: var(--bg); z-index: 100;
246
+ display: flex; align-items: center; justify-content: center;
247
+ flex-direction: column; gap: 16px;
248
+ }
249
+ #loading .spinner {
250
+ width: 32px; height: 32px; border: 3px solid var(--border);
251
+ border-top-color: var(--accent); border-radius: 50%;
252
+ animation: spin 0.8s linear infinite;
253
+ }
254
+ @keyframes spin { to { transform: rotate(360deg); } }
255
+ #loading .load-text { color: var(--text-dim); font-size: 14px; }
256
+ #loading.hidden { display: none; }
257
+ </style>
258
+ </head>
259
+ <body>
260
+
261
+ <div id="loading">
262
+ <div class="spinner"></div>
263
+ <div class="load-text">Loading papers...</div>
264
+ </div>
265
+
266
+ <div id="app">
267
+ <!-- Paper selector panel (left) -->
268
+ <div id="paper-panel">
269
+ <div id="paper-panel-header">
270
+ <h2>Papers</h2>
271
+ <input type="text" id="paper-search" placeholder="Filter papers...">
272
+ <div id="paper-count"></div>
273
+ </div>
274
+ <div id="paper-list"></div>
275
+ </div>
276
+ <button id="toggle-paper-panel">&#9664;</button>
277
+
278
+ <div id="graph-container">
279
+ <div id="toolbar">
280
+ <h1 id="paper-title">Select a paper</h1>
281
+ <div class="spacer"></div>
282
+ <input type="text" id="search" placeholder="Search nodes...">
283
+ <div class="toolbar-group" id="importance-filters"></div>
284
+ <div class="toolbar-group">
285
+ <button class="filter-btn" id="toggle-secondary">Secondary edges</button>
286
+ </div>
287
+ <div class="toolbar-group" id="layout-btns">
288
+ <button class="filter-btn active" data-layout="tree">Tree</button>
289
+ <button class="filter-btn" data-layout="force">Force</button>
290
+ </div>
291
+ </div>
292
+ <svg id="svg"></svg>
293
+ <div id="tooltip">
294
+ <div class="tt-type"></div>
295
+ <div class="tt-title"></div>
296
+ <div class="tt-summary"></div>
297
+ </div>
298
+ <div id="legend"></div>
299
+ </div>
300
+ <div id="detail-panel">
301
+ <div class="panel-empty">Select a paper, then click a node</div>
302
+ </div>
303
+ <button id="toggle-panel">&#9654;</button>
304
+ </div>
305
+
306
+ <script>
307
+ // --- Color scheme ---
308
+ const TYPE_COLORS = {
309
+ overview: '#7c8aff',
310
+ motivation: '#c78aff',
311
+ discussion: '#ff8ac7',
312
+ remark: '#ff8a8a',
313
+ definition: '#52d6a8',
314
+ assumption: '#8adaff',
315
+ claim: '#ffd08a',
316
+ theorem: '#ff6b6b',
317
+ lemma: '#ffa36b',
318
+ proposition: '#ffda6b',
319
+ corollary: '#6bffc4',
320
+ conjecture: '#ff6bda',
321
+ proof_technique: '#6bc4ff',
322
+ example: '#a8d86b',
323
+ construction: '#d8a86b',
324
+ };
325
+ const RELATION_COLORS = {
326
+ depends_on: '#ff8a6b',
327
+ elaborates: '#8ac4ff',
328
+ proves: '#6bffc4',
329
+ motivates: '#c78aff',
330
+ instantiates: '#ffd08a',
331
+ generalizes: '#ff6bda',
332
+ cites: '#8adaff',
333
+ };
334
+
335
+ // --- HuggingFace dataset config ---
336
+ const HF_DATASET = 'AI-Math-TCS/tcs_dags';
337
+ const HF_CONFIG = 'FOCS_2025';
338
+ const HF_SPLIT = 'gpt_5_4';
339
+ const HF_API = 'https://datasets-server.huggingface.co/rows';
340
+
341
+ // --- Globals ---
342
+ let papers = [];
343
+ let dagCache = {}; // paper_id -> dag JSON string (cached from initial fetch)
344
+ let activePaperId = null;
345
+ let dag = null;
346
+ let nodes = [], links = [];
347
+ let simulation = null;
348
+ let svg, g, linkG, nodeG, labelG;
349
+ let zoom;
350
+ let selectedNode = null;
351
+ let currentLayout = 'tree';
352
+ let importanceFilter = new Set(['critical_path','supporting','context']);
353
+ let searchTerm = '';
354
+ let nodeMap = {};
355
+ let depthMap = {};
356
+ let width, height;
357
+ let currentZoomScale = 1;
358
+ let hoveredNode = null;
359
+ let showSecondaryEdges = false;
360
+ let treeChildren = {};
361
+ let treeRoot = null;
362
+ let controlsInitialized = false;
363
+
364
+ // --- Paper panel ---
365
+ async function loadPaperIndex() {
366
+ const loadText = document.querySelector('#loading .load-text');
367
+ try {
368
+ loadText.textContent = 'Fetching papers from HuggingFace...';
369
+ const url = `${HF_API}?dataset=${encodeURIComponent(HF_DATASET)}&config=${HF_CONFIG}&split=${HF_SPLIT}&offset=0&length=100`;
370
+ const resp = await fetch(url);
371
+ if (!resp.ok) throw new Error(`API error: ${resp.status}`);
372
+ const data = await resp.json();
373
+
374
+ loadText.textContent = `Processing ${data.rows.length} papers...`;
375
+ papers = [];
376
+ for (const entry of data.rows) {
377
+ const row = entry.row;
378
+ dagCache[row.paper_id] = row.dag;
379
+ papers.push({
380
+ paper_id: row.paper_id,
381
+ title: row.title,
382
+ authors: row.authors,
383
+ year: row.year,
384
+ venue: row.venue,
385
+ num_nodes: row.num_nodes,
386
+ num_edges: row.num_edges,
387
+ });
388
+ }
389
+ } catch (e) {
390
+ loadText.textContent = `Error loading data: ${e.message}`;
391
+ return;
392
+ }
393
+
394
+ renderPaperList();
395
+ document.getElementById('loading').classList.add('hidden');
396
+
397
+ // If URL has a paper_id param, load it
398
+ const params = new URLSearchParams(window.location.search);
399
+ const pid = params.get('paper');
400
+ if (pid && papers.some(p => p.paper_id === pid)) {
401
+ loadPaper(pid);
402
+ }
403
+ }
404
+
405
+ function renderPaperList(filter) {
406
+ const list = document.getElementById('paper-list');
407
+ const term = (filter || '').toLowerCase();
408
+ const filtered = papers.filter(p =>
409
+ !term ||
410
+ p.title.toLowerCase().includes(term) ||
411
+ p.paper_id.toLowerCase().includes(term) ||
412
+ p.authors.toLowerCase().includes(term)
413
+ );
414
+
415
+ document.getElementById('paper-count').textContent = `${filtered.length} of ${papers.length} papers`;
416
+
417
+ list.innerHTML = filtered.map(p => `
418
+ <div class="paper-item ${p.paper_id === activePaperId ? 'active' : ''}"
419
+ data-id="${p.paper_id}">
420
+ <div class="paper-title">${esc(p.title)}</div>
421
+ <div class="paper-meta">
422
+ <span>${p.num_nodes} nodes</span>
423
+ <span>${p.num_edges} edges</span>
424
+ <span>${p.venue || ''}</span>
425
+ </div>
426
+ </div>
427
+ `).join('');
428
+
429
+ list.querySelectorAll('.paper-item').forEach(el => {
430
+ el.addEventListener('click', () => loadPaper(el.dataset.id));
431
+ });
432
+ }
433
+
434
+ async function loadPaper(paperId) {
435
+ if (paperId === activePaperId) return;
436
+ activePaperId = paperId;
437
+
438
+ // Stop any running simulation from the previous paper
439
+ if (simulation) { simulation.stop(); simulation = null; }
440
+
441
+ // Update URL without reload
442
+ const url = new URL(window.location);
443
+ url.searchParams.set('paper', paperId);
444
+ history.replaceState(null, '', url);
445
+
446
+ // Highlight in list
447
+ document.querySelectorAll('.paper-item').forEach(el => {
448
+ el.classList.toggle('active', el.dataset.id === paperId);
449
+ });
450
+
451
+ // Load DAG from cache
452
+ dag = JSON.parse(dagCache[paperId]);
453
+
454
+ document.getElementById('paper-title').textContent = dag.paper_title || papers.find(p => p.paper_id === paperId)?.title || paperId;
455
+ document.title = (dag.paper_title || paperId) + ' β€” DAG Viewer';
456
+
457
+ // Normalize node/edge IDs: some HF data has unpadded IDs (node_9 vs node_09)
458
+ normalizeDagIds();
459
+
460
+ nodeMap = {};
461
+ dag.nodes.forEach(n => nodeMap[n.id] = n);
462
+
463
+ selectedNode = null;
464
+ document.getElementById('detail-panel').innerHTML = '<div class="panel-empty">Click a node to view details</div>';
465
+
466
+ buildSpanningTree();
467
+ setupGraph();
468
+ if (!controlsInitialized) {
469
+ setupControls();
470
+ controlsInitialized = true;
471
+ }
472
+ buildLegend();
473
+ applyLayout();
474
+ }
475
+
476
+ // --- Spanning tree ---
477
+ // Build a spanning tree via BFS. Primary edges alone often leave many
478
+ // disconnected components in the HF data, so we do a two-phase BFS:
479
+ // 1. Traverse primary edges from the root.
480
+ // 2. For any nodes still unreached, use ALL edges (including secondary)
481
+ // to connect them into the tree, attaching each new component to the
482
+ // nearest already-visited node.
483
+ function buildSpanningTree() {
484
+ treeChildren = {};
485
+ depthMap = {};
486
+
487
+ // Build adjacency lists: primary-only and all-edges
488
+ const primaryNeighbors = {};
489
+ const allNeighbors = {};
490
+ dag.edges.forEach(e => {
491
+ allNeighbors[e.from] = allNeighbors[e.from] || [];
492
+ allNeighbors[e.from].push(e.to);
493
+ allNeighbors[e.to] = allNeighbors[e.to] || [];
494
+ allNeighbors[e.to].push(e.from);
495
+ if (e.is_primary) {
496
+ primaryNeighbors[e.from] = primaryNeighbors[e.from] || [];
497
+ primaryNeighbors[e.from].push(e.to);
498
+ primaryNeighbors[e.to] = primaryNeighbors[e.to] || [];
499
+ primaryNeighbors[e.to].push(e.from);
500
+ }
501
+ });
502
+
503
+ treeRoot = dag.nodes.find(n => n.type === 'overview') || dag.nodes[0];
504
+
505
+ const visited = new Set();
506
+ const queue = [treeRoot.id];
507
+ visited.add(treeRoot.id);
508
+ depthMap[treeRoot.id] = 0;
509
+
510
+ // Phase 1: BFS over primary edges
511
+ function bfs(neighbors) {
512
+ while (queue.length) {
513
+ const id = queue.shift();
514
+ const candidates = neighbors[id] || [];
515
+ for (const cid of candidates) {
516
+ if (!visited.has(cid)) {
517
+ visited.add(cid);
518
+ treeChildren[id] = treeChildren[id] || [];
519
+ treeChildren[id].push(cid);
520
+ depthMap[cid] = depthMap[id] + 1;
521
+ queue.push(cid);
522
+ }
523
+ }
524
+ }
525
+ }
526
+
527
+ bfs(primaryNeighbors);
528
+
529
+ // Phase 2: connect unreached nodes via secondary edges.
530
+ // For each unreached node that has a secondary-edge neighbor already in the
531
+ // tree, attach it there and BFS its primary-connected component.
532
+ if (visited.size < dag.nodes.length) {
533
+ // Iterate until all nodes are reached (each pass may reveal new bridges)
534
+ let changed = true;
535
+ while (changed && visited.size < dag.nodes.length) {
536
+ changed = false;
537
+ for (const n of dag.nodes) {
538
+ if (visited.has(n.id)) continue;
539
+ // Look for any edge (primary or secondary) connecting to visited set
540
+ const nbs = allNeighbors[n.id] || [];
541
+ const bridge = nbs.find(nb => visited.has(nb));
542
+ if (bridge) {
543
+ visited.add(n.id);
544
+ treeChildren[bridge] = treeChildren[bridge] || [];
545
+ treeChildren[bridge].push(n.id);
546
+ depthMap[n.id] = depthMap[bridge] + 1;
547
+ queue.push(n.id);
548
+ // BFS from this node using primary edges to pull in its component
549
+ bfs(primaryNeighbors);
550
+ changed = true;
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ // Final fallback: any truly isolated nodes attach to root
557
+ dag.nodes.forEach(n => {
558
+ if (!visited.has(n.id)) {
559
+ visited.add(n.id);
560
+ treeChildren[treeRoot.id] = treeChildren[treeRoot.id] || [];
561
+ treeChildren[treeRoot.id].push(n.id);
562
+ depthMap[n.id] = 1;
563
+ }
564
+ });
565
+ }
566
+
567
+ function setupGraph() {
568
+ const container = document.getElementById('graph-container');
569
+ width = container.clientWidth;
570
+ height = container.clientHeight;
571
+
572
+ svg = d3.select('#svg');
573
+ svg.selectAll('*').remove();
574
+
575
+ const defs = svg.append('defs');
576
+ ['primary','secondary','highlighted'].forEach(cls => {
577
+ defs.append('marker')
578
+ .attr('id', `arrow-${cls}`)
579
+ .attr('viewBox', '0 -4 8 8')
580
+ .attr('refX', 20).attr('refY', 0)
581
+ .attr('markerWidth', 6).attr('markerHeight', 6)
582
+ .attr('orient', 'auto')
583
+ .append('path')
584
+ .attr('d', 'M0,-3L7,0L0,3')
585
+ .attr('class', `arrowhead ${cls}`);
586
+ });
587
+
588
+ g = svg.append('g');
589
+ linkG = g.append('g').attr('class', 'links');
590
+ nodeG = g.append('g').attr('class', 'nodes');
591
+ labelG = g.append('g').attr('class', 'labels');
592
+
593
+ zoom = d3.zoom()
594
+ .scaleExtent([0.1, 4])
595
+ .on('zoom', e => {
596
+ g.attr('transform', e.transform);
597
+ currentZoomScale = e.transform.k;
598
+ resolveLabels();
599
+ });
600
+ svg.call(zoom);
601
+
602
+ nodes = dag.nodes.map(n => ({
603
+ id: n.id, data: n,
604
+ radius: n.type === 'overview' ? 16 : (n.importance === 'critical_path' ? 12 : 9),
605
+ }));
606
+
607
+ links = dag.edges.map(e => ({
608
+ source: e.from,
609
+ target: e.to,
610
+ data: e,
611
+ }));
612
+
613
+ renderLinks();
614
+ renderNodes();
615
+ }
616
+
617
+ function renderLinks() {
618
+ const linkSel = linkG.selectAll('path.link').data(links, d => `${d.source.id||d.source}-${d.target.id||d.target}`);
619
+ linkSel.exit().remove();
620
+ linkSel.enter().append('path')
621
+ .attr('class', d => `link ${d.data.is_primary ? 'primary' : 'secondary'}`)
622
+ .attr('marker-end', d => `url(#arrow-${d.data.is_primary ? 'primary' : 'secondary'})`);
623
+ }
624
+
625
+ function renderNodes() {
626
+ const circleSel = nodeG.selectAll('circle.node-circle').data(nodes, d => d.id);
627
+ circleSel.exit().remove();
628
+ circleSel.enter().append('circle')
629
+ .attr('class', 'node-circle')
630
+ .attr('r', d => d.radius)
631
+ .attr('fill', d => TYPE_COLORS[d.data.type] || '#888')
632
+ .attr('stroke', d => d3.color(TYPE_COLORS[d.data.type] || '#888').darker(0.5))
633
+ .on('click', (e, d) => { e.stopPropagation(); selectNode(d); })
634
+ .on('mouseenter', (e, d) => { hoveredNode = d; showTooltip(e, d); resolveLabels(); })
635
+ .on('mousemove', (e) => moveTooltip(e))
636
+ .on('mouseleave', () => { hoveredNode = null; hideTooltip(); resolveLabels(); })
637
+ .call(d3.drag()
638
+ .on('start', dragStart)
639
+ .on('drag', dragged)
640
+ .on('end', dragEnd));
641
+
642
+ const labelSel = labelG.selectAll('text.node-label').data(nodes, d => d.id);
643
+ labelSel.exit().remove();
644
+ labelSel.enter().append('text')
645
+ .attr('class', 'node-label')
646
+ .attr('dy', d => d.radius + 14)
647
+ .text(d => truncate(d.data.title, 28));
648
+ }
649
+
650
+ function applyLayout() {
651
+ if (simulation) simulation.stop();
652
+ if (currentLayout === 'tree') applyTreeLayout();
653
+ else applyForceLayout();
654
+ }
655
+
656
+ function applyTreeLayout() {
657
+ if (!treeRoot) { applyForceLayout(); return; }
658
+
659
+ function buildHier(id) {
660
+ const ch = (treeChildren[id] || []).map(cid => buildHier(cid));
661
+ return { id, children: ch.length ? ch : undefined };
662
+ }
663
+
664
+ const hierData = buildHier(treeRoot.id);
665
+ const root = d3.hierarchy(hierData);
666
+
667
+ const leafCount = root.leaves().length;
668
+ const hSpacing = Math.max(60, Math.min(120, width / (leafCount + 1)));
669
+ const treeLayout = d3.tree().nodeSize([hSpacing, 140]);
670
+ treeLayout(root);
671
+
672
+ const posMap = {};
673
+ root.each(d => { posMap[d.data.id] = { x: d.x + width/2, y: d.y + 80 }; });
674
+
675
+ nodes.forEach(n => {
676
+ const pos = posMap[n.id] || { x: width/2, y: height - 80 };
677
+ n.fx = pos.x;
678
+ n.fy = pos.y;
679
+ n.x = n.fx;
680
+ n.y = n.fy;
681
+ });
682
+
683
+ // Apply positions to SVG immediately before starting the simulation,
684
+ // so that fitToView sees correct bounding box.
685
+ tick();
686
+
687
+ simulation = d3.forceSimulation(nodes)
688
+ .force('link', d3.forceLink(links).id(d => d.id).strength(0))
689
+ .alphaDecay(0.1)
690
+ .on('tick', tick);
691
+
692
+ updateVisibility();
693
+ fitToView(600);
694
+ }
695
+
696
+ function applyForceLayout() {
697
+ const maxDepth = Math.max(...Object.values(depthMap));
698
+ const ySpacing = height / (maxDepth + 2);
699
+
700
+ nodes.forEach(n => { n.fx = null; n.fy = null; });
701
+
702
+ simulation = d3.forceSimulation(nodes)
703
+ .force('link', d3.forceLink(links).id(d => d.id).distance(100).strength(d => d.data.is_primary ? 0.7 : 0.1))
704
+ .force('charge', d3.forceManyBody().strength(-300))
705
+ .force('x', d3.forceX(width/2).strength(0.05))
706
+ .force('y', d3.forceY(d => (depthMap[d.id] || 5) * ySpacing + 60).strength(0.3))
707
+ .force('collision', d3.forceCollide().radius(d => d.radius + 12))
708
+ .alphaDecay(0.02)
709
+ .on('tick', tick);
710
+
711
+ updateVisibility();
712
+ setTimeout(() => fitToView(600), 1500);
713
+ }
714
+
715
+ function tick() {
716
+ linkG.selectAll('path.link')
717
+ .attr('d', d => {
718
+ const sx = d.source.x, sy = d.source.y;
719
+ const tx = d.target.x, ty = d.target.y;
720
+ if (!d.data.is_primary) {
721
+ const mx = (sx + tx) / 2 + (ty - sy) * 0.15;
722
+ const my = (sy + ty) / 2 - (tx - sx) * 0.15;
723
+ return `M${sx},${sy}Q${mx},${my},${tx},${ty}`;
724
+ }
725
+ return `M${sx},${sy}L${tx},${ty}`;
726
+ });
727
+
728
+ nodeG.selectAll('circle.node-circle')
729
+ .attr('cx', d => d.x).attr('cy', d => d.y);
730
+
731
+ labelG.selectAll('text.node-label')
732
+ .attr('x', d => d.x).attr('y', d => d.y);
733
+
734
+ resolveLabels();
735
+ }
736
+
737
+ function _labelPriority(d) {
738
+ if (selectedNode && d.id === selectedNode.id) return 100;
739
+ if (hoveredNode && d.id === hoveredNode.id) return 90;
740
+ if (selectedNode) {
741
+ const isConn = links.some(l => {
742
+ const s = l.source.id||l.source, t = l.target.id||l.target;
743
+ return (s === selectedNode.id && t === d.id) || (t === selectedNode.id && s === d.id);
744
+ });
745
+ if (isConn) return 80;
746
+ }
747
+ if (d.data.type === 'overview') return 70;
748
+ if (d.data.importance === 'critical_path') return 60;
749
+ if (d.data.importance === 'supporting') return 40;
750
+ return 20;
751
+ }
752
+
753
+ function resolveLabels() {
754
+ const labels = labelG.selectAll('text.node-label');
755
+ const CHAR_W = 6.5;
756
+ const LABEL_H = 16;
757
+
758
+ const sorted = [...nodes].sort((a, b) => _labelPriority(b) - _labelPriority(a));
759
+ const placed = [];
760
+ const visibleIds = new Set();
761
+
762
+ for (const nd of sorted) {
763
+ const text = truncate(nd.data.title, 28);
764
+ const hw = (text.length * CHAR_W / 2) / currentZoomScale;
765
+ const hh = (LABEL_H / 2) / currentZoomScale;
766
+ const cx = nd.x;
767
+ const cy = nd.y + nd.radius + 14 / currentZoomScale;
768
+
769
+ let overlaps = false;
770
+ for (const p of placed) {
771
+ if (Math.abs(cx - p.x) < (hw + p.hw) && Math.abs(cy - p.y) < (hh + p.hh)) {
772
+ overlaps = true;
773
+ break;
774
+ }
775
+ }
776
+
777
+ const force = _labelPriority(nd) >= 70;
778
+ if (!overlaps || force) {
779
+ placed.push({ x: cx, y: cy, hw, hh });
780
+ visibleIds.add(nd.id);
781
+ }
782
+ }
783
+
784
+ labels.attr('opacity', d => visibleIds.has(d.id) ? 1 : 0);
785
+ }
786
+
787
+ // --- Interaction ---
788
+ function selectNode(d) {
789
+ if (selectedNode === d) { deselectNode(); return; }
790
+ selectedNode = d;
791
+
792
+ const connectedIds = new Set([d.id]);
793
+ links.forEach(l => {
794
+ const sid = l.source.id || l.source;
795
+ const tid = l.target.id || l.target;
796
+ if (sid === d.id) connectedIds.add(tid);
797
+ if (tid === d.id) connectedIds.add(sid);
798
+ });
799
+
800
+ nodeG.selectAll('circle.node-circle')
801
+ .classed('selected', n => n.id === d.id)
802
+ .classed('faded', n => !connectedIds.has(n.id));
803
+ labelG.selectAll('text.node-label')
804
+ .classed('faded', n => !connectedIds.has(n.id));
805
+ linkG.selectAll('path.link')
806
+ .classed('highlighted', l => (l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id)
807
+ .classed('faded', l => (l.source.id||l.source) !== d.id && (l.target.id||l.target) !== d.id)
808
+ .attr('marker-end', l => {
809
+ const involved = (l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id;
810
+ return involved ? 'url(#arrow-highlighted)' : `url(#arrow-${l.data.is_primary?'primary':'secondary'})`;
811
+ });
812
+
813
+ showDetailPanel(d.data);
814
+ }
815
+
816
+ function deselectNode() {
817
+ selectedNode = null;
818
+ nodeG.selectAll('circle.node-circle').classed('selected', false).classed('faded', false);
819
+ labelG.selectAll('text.node-label').classed('faded', false);
820
+ linkG.selectAll('path.link')
821
+ .classed('highlighted', false).classed('faded', false)
822
+ .attr('marker-end', l => `url(#arrow-${l.data.is_primary?'primary':'secondary'})`);
823
+ document.getElementById('detail-panel').innerHTML = '<div class="panel-empty">Click a node to view details</div>';
824
+ }
825
+
826
+ function showDetailPanel(n) {
827
+ const panel = document.getElementById('detail-panel');
828
+ const color = TYPE_COLORS[n.type] || '#888';
829
+
830
+ const inEdges = dag.edges.filter(e => e.from === n.id);
831
+ const outEdges = dag.edges.filter(e => e.to === n.id);
832
+
833
+ let html = `
834
+ <div class="panel-header">
835
+ <span class="node-type-badge" style="background:${color}22;color:${color}">${n.type}</span>
836
+ <span class="importance-badge ${n.importance}">${(n.importance || '').replace('_',' ')}</span>
837
+ <h2>${esc(n.title)}</h2>
838
+ <div class="node-id">${n.id}${n.section_ref ? ' &middot; ' + esc(n.section_ref) : ''}</div>
839
+ </div>
840
+ <div class="panel-body">
841
+ <div class="panel-section">
842
+ <h3>Summary</h3>
843
+ <p>${esc(n.summary)}</p>
844
+ </div>`;
845
+
846
+ if (n.formal_statement) {
847
+ html += `
848
+ <div class="panel-section">
849
+ <h3>Formal Statement</h3>
850
+ <div class="latex-block" data-latex="formal_statement"></div>
851
+ </div>`;
852
+ }
853
+
854
+ if (n.proof_type && n.proof_content) {
855
+ html += `
856
+ <div class="panel-section">
857
+ <h3>Proof / Derivation</h3>
858
+ <span class="proof-type-badge ${n.proof_type}">${n.proof_type}</span>
859
+ <div class="latex-block" data-latex="proof_content"></div>
860
+ </div>`;
861
+ }
862
+
863
+ html += `
864
+ <div class="panel-section">
865
+ <h3>Metadata</h3>
866
+ <div class="meta-grid">
867
+ <span class="meta-key">Source</span><span class="meta-val">${n.source}${n.citation ? ' &mdash; ' + esc(n.citation) : ''}</span>
868
+ ${n.citation_full ? `<span class="meta-key">Reference</span><span class="meta-val">${esc(n.citation_full)}</span>` : ''}
869
+ <span class="meta-key">Excerpt</span><span class="meta-val">${esc(n.source_text_excerpt || '')}</span>
870
+ </div>
871
+ </div>`;
872
+
873
+ if (inEdges.length) {
874
+ html += `
875
+ <div class="panel-section">
876
+ <h3>Parents (${n.id} &rarr;)</h3>
877
+ <ul class="edge-list">${inEdges.map(e => edgeItem(e, e.to)).join('')}</ul>
878
+ </div>`;
879
+ }
880
+ if (outEdges.length) {
881
+ html += `
882
+ <div class="panel-section">
883
+ <h3>Children (&rarr; ${n.id})</h3>
884
+ <ul class="edge-list">${outEdges.map(e => edgeItem(e, e.from)).join('')}</ul>
885
+ </div>`;
886
+ }
887
+
888
+ html += '</div>';
889
+ panel.innerHTML = html;
890
+
891
+ panel.querySelectorAll('[data-latex]').forEach(el => {
892
+ const field = el.dataset.latex;
893
+ if (!n[field]) return;
894
+ let text = n[field];
895
+ // If text already has $ delimiters, use as-is; otherwise add them
896
+ if (!/\$/.test(text)) {
897
+ text = addMathDelimiters(text);
898
+ }
899
+ el.textContent = text;
900
+ });
901
+
902
+ renderLatex(panel);
903
+
904
+ panel.querySelectorAll('.edge-item').forEach(el => {
905
+ el.addEventListener('click', () => {
906
+ const nid = el.dataset.nodeid;
907
+ const nd = nodes.find(n => n.id === nid);
908
+ if (nd) selectNode(nd);
909
+ });
910
+ });
911
+ }
912
+
913
+ function renderLatex(el) {
914
+ if (typeof renderMathInElement !== 'function') {
915
+ setTimeout(() => renderLatex(el), 100);
916
+ return;
917
+ }
918
+ renderMathInElement(el, {
919
+ delimiters: [
920
+ { left: '$$', right: '$$', display: true },
921
+ { left: '$', right: '$', display: false },
922
+ { left: '\\[', right: '\\]', display: true },
923
+ { left: '\\(', right: '\\)', display: false },
924
+ ],
925
+ throwOnError: false,
926
+ });
927
+ }
928
+
929
+ function edgeItem(edge, otherId) {
930
+ const other = nodeMap[otherId];
931
+ const name = other ? other.title : otherId;
932
+ const color = other ? TYPE_COLORS[other.type] : '#888';
933
+ const relColor = RELATION_COLORS[edge.relation] || '#888';
934
+ return `<li class="edge-item" data-nodeid="${otherId}">
935
+ <span class="edge-relation" style="color:${relColor}">${edge.relation}</span>
936
+ <span style="color:${color}">${esc(truncate(name, 40))}</span>
937
+ ${edge.is_primary ? '<span class="edge-primary">PRIMARY</span>' : ''}
938
+ ${edge.description ? '<br><span style="color:var(--text-dim);font-size:11px">' + esc(edge.description) + '</span>' : ''}
939
+ </li>`;
940
+ }
941
+
942
+ // --- Tooltip ---
943
+ function showTooltip(event, d) {
944
+ const tt = document.getElementById('tooltip');
945
+ tt.querySelector('.tt-type').textContent = d.data.type;
946
+ tt.querySelector('.tt-title').textContent = d.data.title;
947
+ tt.querySelector('.tt-summary').textContent = truncate(d.data.summary, 120);
948
+ tt.classList.add('show');
949
+ moveTooltip(event);
950
+ }
951
+ function moveTooltip(event) {
952
+ const tt = document.getElementById('tooltip');
953
+ tt.style.left = (event.clientX + 12) + 'px';
954
+ tt.style.top = (event.clientY - 10) + 'px';
955
+ }
956
+ function hideTooltip() {
957
+ document.getElementById('tooltip').classList.remove('show');
958
+ }
959
+
960
+ // --- Controls ---
961
+ function setupControls() {
962
+ svg.on('click', () => deselectNode());
963
+
964
+ // Node search
965
+ document.getElementById('search').addEventListener('input', e => {
966
+ searchTerm = e.target.value.toLowerCase();
967
+ updateVisibility();
968
+ });
969
+
970
+ // Paper search
971
+ document.getElementById('paper-search').addEventListener('input', e => {
972
+ renderPaperList(e.target.value);
973
+ });
974
+
975
+ // Importance filters
976
+ const filterContainer = document.getElementById('importance-filters');
977
+ ['critical_path','supporting','context'].forEach(imp => {
978
+ const btn = document.createElement('button');
979
+ btn.className = 'filter-btn active';
980
+ btn.textContent = imp.replace('_',' ');
981
+ btn.dataset.importance = imp;
982
+ btn.addEventListener('click', () => {
983
+ if (importanceFilter.has(imp)) { importanceFilter.delete(imp); btn.classList.remove('active'); }
984
+ else { importanceFilter.add(imp); btn.classList.add('active'); }
985
+ updateVisibility();
986
+ });
987
+ filterContainer.appendChild(btn);
988
+ });
989
+
990
+ // Secondary edges toggle
991
+ document.getElementById('toggle-secondary').addEventListener('click', () => {
992
+ showSecondaryEdges = !showSecondaryEdges;
993
+ document.getElementById('toggle-secondary').classList.toggle('active', showSecondaryEdges);
994
+ updateVisibility();
995
+ });
996
+
997
+ // Layout toggle
998
+ document.querySelectorAll('#layout-btns .filter-btn').forEach(btn => {
999
+ btn.addEventListener('click', () => {
1000
+ document.querySelectorAll('#layout-btns .filter-btn').forEach(b => b.classList.remove('active'));
1001
+ btn.classList.add('active');
1002
+ currentLayout = btn.dataset.layout;
1003
+ applyLayout();
1004
+ });
1005
+ });
1006
+
1007
+ // Right detail panel toggle
1008
+ document.getElementById('toggle-panel').addEventListener('click', () => {
1009
+ const panel = document.getElementById('detail-panel');
1010
+ panel.classList.toggle('collapsed');
1011
+ const btn = document.getElementById('toggle-panel');
1012
+ btn.innerHTML = panel.classList.contains('collapsed') ? '&#9664;' : '&#9654;';
1013
+ btn.style.right = panel.classList.contains('collapsed') ? '0' : '640px';
1014
+ });
1015
+
1016
+ // Left paper panel toggle
1017
+ document.getElementById('toggle-paper-panel').addEventListener('click', () => {
1018
+ const panel = document.getElementById('paper-panel');
1019
+ panel.classList.toggle('collapsed');
1020
+ const btn = document.getElementById('toggle-paper-panel');
1021
+ btn.innerHTML = panel.classList.contains('collapsed') ? '&#9654;' : '&#9664;';
1022
+ btn.style.left = panel.classList.contains('collapsed') ? '0' : '320px';
1023
+ });
1024
+
1025
+ // Resize
1026
+ window.addEventListener('resize', () => {
1027
+ width = document.getElementById('graph-container').clientWidth;
1028
+ height = document.getElementById('graph-container').clientHeight;
1029
+ });
1030
+ }
1031
+
1032
+ function updateVisibility() {
1033
+ const visible = new Set();
1034
+ nodes.forEach(n => {
1035
+ const matchImportance = importanceFilter.has(n.data.importance);
1036
+ const matchSearch = !searchTerm ||
1037
+ n.data.title.toLowerCase().includes(searchTerm) ||
1038
+ n.data.id.toLowerCase().includes(searchTerm) ||
1039
+ n.data.type.toLowerCase().includes(searchTerm) ||
1040
+ (n.data.summary || '').toLowerCase().includes(searchTerm);
1041
+ if (matchImportance && matchSearch) visible.add(n.id);
1042
+ });
1043
+
1044
+ nodeG.selectAll('circle.node-circle')
1045
+ .attr('opacity', d => visible.has(d.id) ? 1 : 0.1);
1046
+ labelG.selectAll('text.node-label')
1047
+ .attr('opacity', d => visible.has(d.id) ? 1 : 0.08);
1048
+ linkG.selectAll('path.link')
1049
+ .attr('opacity', l => {
1050
+ if (!showSecondaryEdges && !l.data.is_primary) return 0;
1051
+ const sid = l.source.id || l.source;
1052
+ const tid = l.target.id || l.target;
1053
+ return visible.has(sid) && visible.has(tid) ? 1 : 0.05;
1054
+ });
1055
+ }
1056
+
1057
+ // --- Drag ---
1058
+ function dragStart(event, d) {
1059
+ if (!event.active && simulation) simulation.alphaTarget(0.1).restart();
1060
+ d.fx = d.x; d.fy = d.y;
1061
+ }
1062
+ function dragged(event, d) {
1063
+ d.fx = event.x; d.fy = event.y;
1064
+ }
1065
+ function dragEnd(event, d) {
1066
+ if (!event.active && simulation) simulation.alphaTarget(0);
1067
+ if (currentLayout === 'force') { d.fx = null; d.fy = null; }
1068
+ }
1069
+
1070
+ // --- Fit view ---
1071
+ function fitToView(duration) {
1072
+ const bounds = g.node().getBBox();
1073
+ if (bounds.width === 0 || bounds.height === 0) return;
1074
+ const pad = 60;
1075
+ const scale = Math.min(
1076
+ (width - pad*2) / bounds.width,
1077
+ (height - pad*2) / bounds.height,
1078
+ 1.5
1079
+ );
1080
+ const tx = width/2 - (bounds.x + bounds.width/2) * scale;
1081
+ const ty = height/2 - (bounds.y + bounds.height/2) * scale;
1082
+ svg.transition().duration(duration)
1083
+ .call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
1084
+ }
1085
+
1086
+ // --- Legend ---
1087
+ function buildLegend() {
1088
+ const present = new Set(dag.nodes.map(n => n.type));
1089
+ const legend = document.getElementById('legend');
1090
+ let html = '<div style="font-weight:600;margin-bottom:4px;font-size:12px">Node Types</div>';
1091
+ Object.entries(TYPE_COLORS).forEach(([type, color]) => {
1092
+ if (!present.has(type)) return;
1093
+ html += `<div class="legend-row"><span class="legend-dot" style="background:${color}"></span>${type}</div>`;
1094
+ });
1095
+ html += '<div style="margin-top:8px;font-weight:600;margin-bottom:4px;font-size:12px">Edges</div>';
1096
+ html += `<div class="legend-row"><span class="legend-line" style="background:#4a5080"></span>Primary</div>`;
1097
+ html += `<div class="legend-row"><span class="legend-line" style="background:#3a3e55;border-top:2px dashed #3a3e55;height:0"></span>Secondary</div>`;
1098
+ legend.innerHTML = html;
1099
+ }
1100
+
1101
+ // --- Helpers ---
1102
+ // Normalize IDs: some HF DAGs have unpadded node refs in edges (node_9 vs node_09).
1103
+ // Build a lookup from the canonical node IDs and fix any edge refs that don't match.
1104
+ function normalizeDagIds() {
1105
+ const nodeIds = new Set(dag.nodes.map(n => n.id));
1106
+ // Build map: unpadded -> padded (e.g. node_9 -> node_09)
1107
+ const fixMap = {};
1108
+ nodeIds.forEach(id => {
1109
+ const m = id.match(/^(node_)0*(\d+)$/);
1110
+ if (m) fixMap[m[1] + m[2]] = id; // node_9 -> node_09
1111
+ });
1112
+ dag.edges = dag.edges.map(e => ({
1113
+ ...e,
1114
+ from: nodeIds.has(e.from) ? e.from : (fixMap[e.from] || e.from),
1115
+ to: nodeIds.has(e.to) ? e.to : (fixMap[e.to] || e.to),
1116
+ }));
1117
+ // Drop edges that still reference non-existent nodes
1118
+ dag.edges = dag.edges.filter(e => nodeIds.has(e.from) && nodeIds.has(e.to));
1119
+ }
1120
+
1121
+ function truncate(s, n) { return s && s.length > n ? s.slice(0, n) + '...' : (s || ''); }
1122
+ function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
1123
+
1124
+ // Wrap math spans in $...$ for text that lacks delimiters.
1125
+ // Splits text into tokens, classifying each as math or prose based on
1126
+ // the presence of math indicators (^, _, {}, \commands, operators, etc.).
1127
+ function addMathDelimiters(text) {
1128
+ // Regex matching a "math span": a sequence that contains math-indicative
1129
+ // characters/patterns, including surrounding variable names and operators.
1130
+ // We match greedily and rely on the join logic to merge adjacent spans.
1131
+ //
1132
+ // Math indicators:
1133
+ // \command ^{...} _{...} single-letter vars with sub/superscript
1134
+ // math symbols: β‰₯ ≀ ∈ βŠ† βŠ‡ ∩ βˆͺ Γ— βŠ— β†’ ← β‰  β‰ˆ βˆ€ βˆƒ βˆ… ∞ β„“ βˆ‚ βˆ‘ ∏ Ξ»
1135
+ // common patterns: f(x), O(n), Pr[...], E[...]
1136
+
1137
+ // If the text has LaTeX commands, use the original line-based approach
1138
+ // but enhanced to also catch bare math
1139
+ return text.split('\n').map(line => {
1140
+ if (/\$/.test(line)) return line; // already delimited
1141
+
1142
+ // Strategy: split on sentence-like boundaries and wrap segments
1143
+ // that look mathematical.
1144
+ const mathIndicators = /[\\^_{}β‰₯β‰€βˆˆβŠ†βŠ‡βŠ‚βŠƒβˆ©βˆͺΓ—βŠ—β†’β†β†¦β‰ β‰ˆβˆ€βˆƒβˆ…βˆžβ„“βˆ‚βˆ‘βˆΞ»ΞΌΟƒΞ΄Ξ΅Ξ±Ξ²Ξ³Ξ”Ξ©Ξ˜Ξ£Ξ Ξ¦Ξ¨]/;
1145
+ const mathPattern = /(?:\\[a-zA-Z]+(?:\{[^}]*\})*|[A-Za-z0-9_]+[\^_]\{[^}]*\}|[A-Za-z0-9_]+[\^_][A-Za-z0-9]|\|[^|]+\||[β‰₯β‰€βˆˆβŠ†βŠ‡βŠ‚βŠƒβˆ©βˆͺΓ—βŠ—β†’β†β†¦β‰ β‰ˆβˆ€βˆƒβˆ…βˆžβ„“βˆ‚βˆ‘βˆΞ»ΞΌΟƒΞ΄Ξ΅Ξ±Ξ²Ξ³Ξ”Ξ©Ξ˜Ξ£Ξ Ξ¦Ξ¨])/;
1146
+
1147
+ // If the line has no math indicators at all, return as-is
1148
+ // Also check for "X = ..." patterns (single capital + equals)
1149
+ if (!mathIndicators.test(line) && !/[A-Z]_/.test(line) && !/\^/.test(line)
1150
+ && !/(?:^|[\s(])[A-Z]\s*=/.test(line)) {
1151
+ return line;
1152
+ }
1153
+
1154
+ // For lines that are mostly math (formal statements), wrap the whole
1155
+ // thing as inline math segments. Split on sentence boundaries: periods
1156
+ // followed by space+capital, or colons/semicolons.
1157
+ const segments = line.split(/(?<=\.\s)(?=[A-Z])|(?<=:\s)|(?<=;\s)/);
1158
+
1159
+ return segments.map(seg => {
1160
+ if (/\$/.test(seg)) return seg; // already has delimiters
1161
+ if (!mathIndicators.test(seg) && !/[A-Z]_/.test(seg) && !/\^/.test(seg) && !/[=<>]/.test(seg)) {
1162
+ return seg; // pure prose
1163
+ }
1164
+
1165
+ // Walk through the segment, wrapping math-like tokens in $...$
1166
+ // We split on word boundaries and classify runs.
1167
+ let result = '';
1168
+ let mathBuf = '';
1169
+ let proseBuf = '';
1170
+
1171
+ // Tokenize: split into "words" preserving whitespace and punctuation
1172
+ const tokens = seg.match(/:=|\\[{}|]|\\[a-zA-Z]+(?:\{[^}]*\})*|[A-Za-z0-9']+(?:[\^_]\{[^}]*\}|[\^_][A-Za-z0-9])*|\{[^}]*\}|[β‰₯β‰€βˆˆβŠ†βŠ‡βŠ‚βŠƒβˆ©βˆͺΓ—βŠ—β†’β†β†¦β‰ β‰ˆβˆ€βˆƒβˆ…βˆžβ„“βˆ‚βˆ‘βˆΞ»ΞΌΟƒΞ΄Ξ΅Ξ±Ξ²Ξ³Ξ”Ξ©Ξ˜Ξ£Ξ Ξ¦Ξ¨]+|[=<>]+|[,;.:!?]+|[/\-+*|]|\(|\)|\[|\]|\s+|./g);
1173
+ if (!tokens) return seg;
1174
+
1175
+ const PROSE_WORDS = new Set([
1176
+ 'a','an','as','at','be','by','do','if','in','is','it','no','of',
1177
+ 'on','or','so','to','up','we','and','are','can','for','has','its',
1178
+ 'may','not','our','the','was','all','but','let','say','set','via',
1179
+ ]);
1180
+
1181
+ function isMathToken(t) {
1182
+ if (!t) return false;
1183
+ if (/^\\/.test(t)) return true; // \command
1184
+ if (/[\^_{}]/.test(t)) return true; // sub/superscript or braces
1185
+ if (/[β‰₯β‰€βˆˆβŠ†βŠ‡βŠ‚βŠƒβˆ©βˆͺΓ—βŠ—β†’β†β†¦β‰ β‰ˆβˆ€βˆƒβˆ…βˆžβ„“βˆ‚βˆ‘βˆΞ»ΞΌΟƒΞ΄Ξ΅Ξ±Ξ²Ξ³Ξ”Ξ©Ξ˜Ξ£Ξ Ξ¦Ξ¨]/.test(t)) return true;
1186
+ if (/^[=<>]+$/.test(t) || t === ':=') return true; // comparison/assignment ops
1187
+ if (/^[A-Z]$/.test(t)) return true; // single capital (likely variable)
1188
+ if (/^[a-z]$/.test(t) && !PROSE_WORDS.has(t)) return true; // single lowercase variable (not a common word)
1189
+ if (/^[0-9]+$/.test(t)) return true; // bare number (in math context)
1190
+ return false;
1191
+ }
1192
+
1193
+ // Track brace depth so we never flush a math span with unbalanced braces
1194
+ let braceDepth = 0;
1195
+
1196
+ function flushMath() {
1197
+ if (mathBuf.trim()) result += '$' + mathBuf.trim() + '$';
1198
+ else result += mathBuf;
1199
+ mathBuf = '';
1200
+ braceDepth = 0;
1201
+ }
1202
+ function flushProse() {
1203
+ result += proseBuf;
1204
+ proseBuf = '';
1205
+ }
1206
+
1207
+ function updateBraceDepth(t) {
1208
+ // Skip escaped braces \{ \} β€” they don't affect grouping depth
1209
+ if (t === '\\{' || t === '\\}') return;
1210
+ for (const ch of t) {
1211
+ if (ch === '{') braceDepth++;
1212
+ if (ch === '}') braceDepth--;
1213
+ }
1214
+ }
1215
+
1216
+ let inMath = false;
1217
+ for (let i = 0; i < tokens.length; i++) {
1218
+ const t = tokens[i];
1219
+ const isWs = /^\s+$/.test(t);
1220
+ const isPunct = /^[,;.:!?]+$/.test(t);
1221
+ const isBracket = /^[(\)\[\]]$/.test(t);
1222
+ const isMathOp = /^[/+*|]$/.test(t);
1223
+ const isMinus = t === '-';
1224
+ const isMath = isMathToken(t);
1225
+
1226
+ if (isWs || isPunct || isBracket || isMathOp || isMinus) {
1227
+ if (inMath) {
1228
+ // Don't break out of math if braces are unbalanced
1229
+ if (braceDepth !== 0) {
1230
+ mathBuf += t;
1231
+ continue;
1232
+ }
1233
+ // Look ahead: if next non-ws token is math, stay in math
1234
+ let nextMath = false;
1235
+ for (let j = i + 1; j < tokens.length; j++) {
1236
+ if (/^\s+$/.test(tokens[j])) continue;
1237
+ nextMath = isMathToken(tokens[j]) || /^[(\)\[\]/+*|]$/.test(tokens[j]);
1238
+ break;
1239
+ }
1240
+ if (nextMath || isBracket || isMathOp) {
1241
+ mathBuf += t;
1242
+ } else {
1243
+ flushMath();
1244
+ inMath = false;
1245
+ proseBuf += t;
1246
+ }
1247
+ } else {
1248
+ proseBuf += t;
1249
+ }
1250
+ } else if (isMath) {
1251
+ if (!inMath) {
1252
+ flushProse();
1253
+ inMath = true;
1254
+ }
1255
+ mathBuf += t;
1256
+ updateBraceDepth(t);
1257
+ } else {
1258
+ // Prose word (multiple chars, not a math token)
1259
+ if (inMath) {
1260
+ // Don't break out if braces are unbalanced
1261
+ if (braceDepth !== 0) {
1262
+ mathBuf += t;
1263
+ continue;
1264
+ }
1265
+ flushMath();
1266
+ inMath = false;
1267
+ }
1268
+ proseBuf += t;
1269
+ }
1270
+ }
1271
+ // Flush remaining
1272
+ if (inMath) flushMath();
1273
+ else flushProse();
1274
+
1275
+ return result;
1276
+ }).join('');
1277
+ }).join('\n');
1278
+ }
1279
+
1280
+ loadPaperIndex();
1281
+ </script>
1282
+ </body>
1283
  </html>
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }