paperlint 13.2.0 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +10 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +2 -2
- package/dist/doctor.js.map +1 -1
- package/dist/skills/assess-novelty/scripts/candidate_page.d.ts +1 -0
- package/dist/skills/assess-novelty/scripts/novelty_reader_brief.d.ts +25 -0
- package/docs/toolchain.md +1 -1
- package/eslint.config.mjs +52 -0
- package/package.json +2 -1
- package/presets/agentverify.md +15 -4
- package/skills/assess-novelty/scripts/candidate_page.css +31 -0
- package/skills/assess-novelty/scripts/candidate_page.html +934 -0
- package/skills/assess-novelty/scripts/candidate_page.js +165 -0
- package/skills/assess-novelty/scripts/candidate_page.test.ts +401 -0
- package/skills/assess-novelty/scripts/candidate_page.ts +194 -0
- package/skills/assess-novelty/scripts/check_novelty.py +230 -0
- package/skills/assess-novelty/scripts/check_novelty.test.ts +490 -0
- package/skills/assess-novelty/scripts/check_novelty_coverage.test.ts +176 -0
- package/skills/assess-novelty/scripts/crop_quote.py +104 -0
- package/skills/assess-novelty/scripts/crop_quote.test.ts +130 -0
- package/skills/assess-novelty/scripts/fixtures/crop-quote/make_sample.py +42 -0
- package/skills/assess-novelty/scripts/fixtures/crop-quote/sample.pdf +106 -0
- package/skills/assess-novelty/scripts/fixtures/novelty/CLAIMS.md +11 -0
- package/skills/assess-novelty/scripts/fixtures/novelty/paper.tex +10 -0
- package/skills/assess-novelty/scripts/fixtures/novelty/siblings/2026-10-27-novelty-refutation.md +8 -0
- package/skills/assess-novelty/scripts/fixtures/novelty/siblings/data/doe-preprint.txt +63 -0
- package/skills/assess-novelty/scripts/fixtures/novelty/siblings/novelty.json +54 -0
- package/skills/assess-novelty/scripts/novelty_candidates.py +136 -0
- package/skills/assess-novelty/scripts/novelty_candidates.test.ts +222 -0
- package/skills/assess-novelty/scripts/novelty_cells.py +88 -0
- package/skills/assess-novelty/scripts/novelty_cells.test.ts +159 -0
- package/skills/assess-novelty/scripts/novelty_coverage.py +55 -0
- package/skills/assess-novelty/scripts/novelty_reader_brief.js +165 -0
- package/skills/assess-novelty/scripts/novelty_reader_brief.test.ts +251 -0
- package/skills/assess-novelty/scripts/novelty_reader_brief.ts +215 -0
- package/skills/assess-novelty/scripts/quote_match.py +34 -0
- package/skills/assess-novelty/scripts/script_edges.test.ts +174 -0
- package/src/doctor.ts +4 -2
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
<title>__TITLE__</title>
|
|
2
|
+
<meta name="claude:viewport-fit-sides" content="cover" />
|
|
3
|
+
<link
|
|
4
|
+
rel="stylesheet"
|
|
5
|
+
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Roboto+Mono:wght@400&display=swap"
|
|
6
|
+
/>
|
|
7
|
+
<style>
|
|
8
|
+
/* Look: Material Ocean, minimal; candidate_page.css is inlined below by candidate_page.ts.
|
|
9
|
+
Layout: sticky header (title, progress, send) over a one-row underline tab strip; one column per tab: the
|
|
10
|
+
work, why it came up, each passage as its page image + the words + what it means, then the one raised block,
|
|
11
|
+
the decision. */
|
|
12
|
+
__OCEAN__ .wrap {
|
|
13
|
+
max-width: 760px;
|
|
14
|
+
margin: 0 auto;
|
|
15
|
+
padding-left: max(16px, env(safe-area-inset-left, 0px));
|
|
16
|
+
padding-right: max(16px, env(safe-area-inset-right, 0px));
|
|
17
|
+
}
|
|
18
|
+
body::before {
|
|
19
|
+
content: "";
|
|
20
|
+
position: fixed;
|
|
21
|
+
inset: 0 0 auto;
|
|
22
|
+
height: env(safe-area-inset-top, 0px);
|
|
23
|
+
z-index: 10;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
}
|
|
26
|
+
header {
|
|
27
|
+
position: sticky;
|
|
28
|
+
top: env(safe-area-inset-top, 0px);
|
|
29
|
+
z-index: 5;
|
|
30
|
+
background: var(--bg);
|
|
31
|
+
border-bottom: 1px solid var(--line);
|
|
32
|
+
}
|
|
33
|
+
header .wrap {
|
|
34
|
+
padding-block: 20px 0;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: 14px;
|
|
38
|
+
}
|
|
39
|
+
.top {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
gap: 12px 24px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
}
|
|
46
|
+
h1 {
|
|
47
|
+
margin: 0;
|
|
48
|
+
font-size: 1.25rem;
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
color: var(--strong);
|
|
51
|
+
line-height: 1.3;
|
|
52
|
+
text-wrap: balance;
|
|
53
|
+
}
|
|
54
|
+
.progress {
|
|
55
|
+
font-family: var(--mono);
|
|
56
|
+
font-size: 0.85rem;
|
|
57
|
+
color: var(--muted);
|
|
58
|
+
font-variant-numeric: tabular-nums;
|
|
59
|
+
}
|
|
60
|
+
.sendrow {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
gap: 12px;
|
|
64
|
+
align-items: center;
|
|
65
|
+
}
|
|
66
|
+
.status {
|
|
67
|
+
font-size: 0.9rem;
|
|
68
|
+
color: var(--muted);
|
|
69
|
+
}
|
|
70
|
+
.status.warn {
|
|
71
|
+
color: var(--mark);
|
|
72
|
+
}
|
|
73
|
+
.status.good {
|
|
74
|
+
color: var(--good);
|
|
75
|
+
}
|
|
76
|
+
nav {
|
|
77
|
+
display: flex;
|
|
78
|
+
gap: 28px;
|
|
79
|
+
overflow-x: auto;
|
|
80
|
+
scrollbar-width: none;
|
|
81
|
+
}
|
|
82
|
+
nav::-webkit-scrollbar {
|
|
83
|
+
display: none;
|
|
84
|
+
}
|
|
85
|
+
.tab {
|
|
86
|
+
font: inherit;
|
|
87
|
+
font-size: 0.95rem;
|
|
88
|
+
background: none;
|
|
89
|
+
border: 0;
|
|
90
|
+
border-bottom: 2px solid transparent;
|
|
91
|
+
color: var(--muted);
|
|
92
|
+
padding: 6px 0 12px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
gap: 8px;
|
|
97
|
+
align-items: center;
|
|
98
|
+
min-height: 44px;
|
|
99
|
+
}
|
|
100
|
+
.tab[aria-selected="true"] {
|
|
101
|
+
color: var(--strong);
|
|
102
|
+
border-bottom-color: var(--accent);
|
|
103
|
+
}
|
|
104
|
+
.dot {
|
|
105
|
+
width: 7px;
|
|
106
|
+
height: 7px;
|
|
107
|
+
border-radius: 50%;
|
|
108
|
+
background: var(--line);
|
|
109
|
+
flex: none;
|
|
110
|
+
}
|
|
111
|
+
.dot.add {
|
|
112
|
+
background: var(--good);
|
|
113
|
+
}
|
|
114
|
+
.dot.cite {
|
|
115
|
+
background: var(--accent);
|
|
116
|
+
}
|
|
117
|
+
.dot.skip {
|
|
118
|
+
background: var(--muted);
|
|
119
|
+
}
|
|
120
|
+
main .wrap {
|
|
121
|
+
padding-block: 32px 96px;
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: var(--gap-section);
|
|
125
|
+
}
|
|
126
|
+
section {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: var(--gap-item);
|
|
130
|
+
}
|
|
131
|
+
section + section {
|
|
132
|
+
border-top: 1px solid var(--line);
|
|
133
|
+
padding-top: var(--gap-section);
|
|
134
|
+
}
|
|
135
|
+
h2 {
|
|
136
|
+
margin: 0;
|
|
137
|
+
font-size: 1.5rem;
|
|
138
|
+
font-weight: 500;
|
|
139
|
+
color: var(--strong);
|
|
140
|
+
line-height: 1.3;
|
|
141
|
+
text-wrap: balance;
|
|
142
|
+
}
|
|
143
|
+
h3 {
|
|
144
|
+
margin: 0;
|
|
145
|
+
font-size: 0.75rem;
|
|
146
|
+
font-weight: 500;
|
|
147
|
+
letter-spacing: 0.08em;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
color: var(--muted);
|
|
150
|
+
}
|
|
151
|
+
p {
|
|
152
|
+
margin: 0;
|
|
153
|
+
max-width: var(--measure);
|
|
154
|
+
}
|
|
155
|
+
.meta {
|
|
156
|
+
font-family: var(--mono);
|
|
157
|
+
font-size: 0.85rem;
|
|
158
|
+
color: var(--muted);
|
|
159
|
+
}
|
|
160
|
+
a {
|
|
161
|
+
color: var(--accent);
|
|
162
|
+
}
|
|
163
|
+
.links {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
gap: 24px;
|
|
167
|
+
}
|
|
168
|
+
.links a {
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
font-weight: 500;
|
|
171
|
+
min-height: 44px;
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
}
|
|
175
|
+
.links a:hover,
|
|
176
|
+
.links a:focus-visible {
|
|
177
|
+
text-decoration: underline;
|
|
178
|
+
}
|
|
179
|
+
figure {
|
|
180
|
+
margin: 0;
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
gap: 12px;
|
|
184
|
+
min-width: 0;
|
|
185
|
+
}
|
|
186
|
+
figure + figure {
|
|
187
|
+
margin-top: calc(var(--gap-section) - var(--gap-item));
|
|
188
|
+
}
|
|
189
|
+
figure img {
|
|
190
|
+
width: 100%;
|
|
191
|
+
height: auto;
|
|
192
|
+
border-radius: 8px;
|
|
193
|
+
background: var(--paper);
|
|
194
|
+
display: block;
|
|
195
|
+
}
|
|
196
|
+
figcaption {
|
|
197
|
+
font-family: var(--mono);
|
|
198
|
+
font-size: 0.85rem;
|
|
199
|
+
color: var(--muted);
|
|
200
|
+
}
|
|
201
|
+
.claimtext {
|
|
202
|
+
color: var(--muted);
|
|
203
|
+
font-size: 0.9rem;
|
|
204
|
+
max-width: var(--measure);
|
|
205
|
+
}
|
|
206
|
+
blockquote {
|
|
207
|
+
margin: 0;
|
|
208
|
+
padding: 12px 16px;
|
|
209
|
+
border-left: 2px solid var(--mark);
|
|
210
|
+
background: var(--mark-bg);
|
|
211
|
+
color: var(--strong);
|
|
212
|
+
max-width: var(--measure);
|
|
213
|
+
}
|
|
214
|
+
.raised {
|
|
215
|
+
background: var(--surface);
|
|
216
|
+
border: 1px solid var(--line);
|
|
217
|
+
border-radius: 12px;
|
|
218
|
+
padding: 20px;
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
gap: var(--gap-item);
|
|
222
|
+
}
|
|
223
|
+
.choices {
|
|
224
|
+
display: grid;
|
|
225
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
226
|
+
gap: 12px;
|
|
227
|
+
}
|
|
228
|
+
.choice {
|
|
229
|
+
font: inherit;
|
|
230
|
+
text-align: left;
|
|
231
|
+
background: transparent;
|
|
232
|
+
border: 1px solid var(--line);
|
|
233
|
+
border-radius: 10px;
|
|
234
|
+
padding: 14px 16px;
|
|
235
|
+
color: var(--text);
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
gap: 4px;
|
|
240
|
+
min-height: 44px;
|
|
241
|
+
}
|
|
242
|
+
.choice b {
|
|
243
|
+
color: var(--strong);
|
|
244
|
+
font-weight: 500;
|
|
245
|
+
}
|
|
246
|
+
.choice span {
|
|
247
|
+
font-size: 0.85rem;
|
|
248
|
+
color: var(--muted);
|
|
249
|
+
}
|
|
250
|
+
.choice[aria-pressed="true"].add {
|
|
251
|
+
border: 2px solid var(--good);
|
|
252
|
+
}
|
|
253
|
+
.choice[aria-pressed="true"].cite {
|
|
254
|
+
border: 2px solid var(--accent);
|
|
255
|
+
}
|
|
256
|
+
.choice[aria-pressed="true"].skip {
|
|
257
|
+
border: 2px solid var(--muted);
|
|
258
|
+
}
|
|
259
|
+
.choice:disabled {
|
|
260
|
+
opacity: 0.5;
|
|
261
|
+
cursor: default;
|
|
262
|
+
}
|
|
263
|
+
label {
|
|
264
|
+
font-size: 0.9rem;
|
|
265
|
+
color: var(--muted);
|
|
266
|
+
}
|
|
267
|
+
textarea {
|
|
268
|
+
font: inherit;
|
|
269
|
+
width: 100%;
|
|
270
|
+
box-sizing: border-box;
|
|
271
|
+
min-height: 72px;
|
|
272
|
+
background: var(--bg);
|
|
273
|
+
color: var(--text);
|
|
274
|
+
border: 1px solid var(--line);
|
|
275
|
+
border-radius: 8px;
|
|
276
|
+
padding: 10px 12px;
|
|
277
|
+
}
|
|
278
|
+
.row {
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-wrap: wrap;
|
|
281
|
+
gap: 12px 20px;
|
|
282
|
+
align-items: center;
|
|
283
|
+
}
|
|
284
|
+
.btn {
|
|
285
|
+
font: inherit;
|
|
286
|
+
font-weight: 500;
|
|
287
|
+
background: var(--accent);
|
|
288
|
+
color: var(--bg);
|
|
289
|
+
border: 1px solid var(--accent);
|
|
290
|
+
border-radius: 8px;
|
|
291
|
+
padding: 10px 18px;
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
min-height: 44px;
|
|
294
|
+
}
|
|
295
|
+
.btn.quiet {
|
|
296
|
+
background: transparent;
|
|
297
|
+
color: var(--accent);
|
|
298
|
+
}
|
|
299
|
+
.btn:disabled {
|
|
300
|
+
opacity: 0.5;
|
|
301
|
+
cursor: default;
|
|
302
|
+
}
|
|
303
|
+
.banner {
|
|
304
|
+
border: 1px solid var(--mark);
|
|
305
|
+
color: var(--mark);
|
|
306
|
+
border-radius: 8px;
|
|
307
|
+
padding: 12px 16px;
|
|
308
|
+
}
|
|
309
|
+
.tablebox {
|
|
310
|
+
overflow-x: auto;
|
|
311
|
+
}
|
|
312
|
+
.own {
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-direction: column;
|
|
315
|
+
gap: 12px;
|
|
316
|
+
}
|
|
317
|
+
.own + .own {
|
|
318
|
+
margin-top: calc(var(--gap-section) - var(--gap-item));
|
|
319
|
+
}
|
|
320
|
+
details {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
gap: 12px;
|
|
324
|
+
}
|
|
325
|
+
details > p + p,
|
|
326
|
+
.own > p + p {
|
|
327
|
+
margin-top: 0;
|
|
328
|
+
}
|
|
329
|
+
summary {
|
|
330
|
+
color: var(--accent);
|
|
331
|
+
font-weight: 500;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
min-height: 44px;
|
|
334
|
+
display: flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
list-style: none;
|
|
337
|
+
}
|
|
338
|
+
summary::-webkit-details-marker {
|
|
339
|
+
display: none;
|
|
340
|
+
}
|
|
341
|
+
details[open] > summary {
|
|
342
|
+
margin-bottom: 12px;
|
|
343
|
+
}
|
|
344
|
+
table {
|
|
345
|
+
border-collapse: collapse;
|
|
346
|
+
width: 100%;
|
|
347
|
+
}
|
|
348
|
+
th,
|
|
349
|
+
td {
|
|
350
|
+
text-align: left;
|
|
351
|
+
padding: 14px 12px;
|
|
352
|
+
border-bottom: 1px solid var(--line);
|
|
353
|
+
vertical-align: top;
|
|
354
|
+
}
|
|
355
|
+
tr:last-child td {
|
|
356
|
+
border-bottom: 0;
|
|
357
|
+
}
|
|
358
|
+
th {
|
|
359
|
+
white-space: nowrap;
|
|
360
|
+
font-size: 0.75rem;
|
|
361
|
+
font-weight: 500;
|
|
362
|
+
letter-spacing: 0.08em;
|
|
363
|
+
text-transform: uppercase;
|
|
364
|
+
color: var(--muted);
|
|
365
|
+
}
|
|
366
|
+
td a {
|
|
367
|
+
text-decoration: none;
|
|
368
|
+
color: var(--strong);
|
|
369
|
+
}
|
|
370
|
+
td .meta {
|
|
371
|
+
margin-top: 4px;
|
|
372
|
+
}
|
|
373
|
+
:focus-visible {
|
|
374
|
+
outline: 2px solid var(--accent);
|
|
375
|
+
outline-offset: 3px;
|
|
376
|
+
}
|
|
377
|
+
@media (prefers-reduced-motion: reduce) {
|
|
378
|
+
* {
|
|
379
|
+
transition: none !important;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
</style>
|
|
383
|
+
<header>
|
|
384
|
+
<div class="wrap">
|
|
385
|
+
<div class="top">
|
|
386
|
+
<h1 id="title"></h1>
|
|
387
|
+
<div class="sendrow">
|
|
388
|
+
<span class="progress" id="progress"></span>
|
|
389
|
+
<button class="btn" id="send" type="button">
|
|
390
|
+
Send decisions to Claude
|
|
391
|
+
</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="status" id="sendstatus"></div>
|
|
395
|
+
<nav id="tabs" role="tablist"></nav>
|
|
396
|
+
</div>
|
|
397
|
+
</header>
|
|
398
|
+
<main><div class="wrap" id="view"></div></main>
|
|
399
|
+
<script type="application/json" id="data">
|
|
400
|
+
__DATA__
|
|
401
|
+
</script>
|
|
402
|
+
<script>
|
|
403
|
+
(function () {
|
|
404
|
+
"use strict";
|
|
405
|
+
var DATA = JSON.parse(document.getElementById("data").textContent);
|
|
406
|
+
var CHOICES = [
|
|
407
|
+
[
|
|
408
|
+
"add",
|
|
409
|
+
"Add as neighbour",
|
|
410
|
+
"I read it in full and add it to the novelty table",
|
|
411
|
+
],
|
|
412
|
+
["cite", "Cite only", "Not a neighbour; the paper should cite it"],
|
|
413
|
+
["skip", "Skip", "Not relevant; say why"],
|
|
414
|
+
];
|
|
415
|
+
var state = {
|
|
416
|
+
saved: {},
|
|
417
|
+
store: "loading",
|
|
418
|
+
drafts: {},
|
|
419
|
+
pending: {},
|
|
420
|
+
failed: {},
|
|
421
|
+
mcp: null,
|
|
422
|
+
send: "",
|
|
423
|
+
};
|
|
424
|
+
var db = null;
|
|
425
|
+
var byId = {};
|
|
426
|
+
DATA.candidates.forEach(function (c) {
|
|
427
|
+
byId[c.id] = c;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
function el(tag, cls, text) {
|
|
431
|
+
var e = document.createElement(tag);
|
|
432
|
+
if (cls) e.className = cls;
|
|
433
|
+
if (text !== undefined && text !== null) e.textContent = text;
|
|
434
|
+
return e;
|
|
435
|
+
}
|
|
436
|
+
function section(heading) {
|
|
437
|
+
var s = el("section");
|
|
438
|
+
if (heading) s.appendChild(el("h3", "", heading));
|
|
439
|
+
return s;
|
|
440
|
+
}
|
|
441
|
+
function decisionOf(id) {
|
|
442
|
+
return state.saved[id] || (byId[id] && byId[id].decision) || null;
|
|
443
|
+
}
|
|
444
|
+
function decided() {
|
|
445
|
+
return DATA.candidates.filter(function (c) {
|
|
446
|
+
return decisionOf(c.id);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function current() {
|
|
450
|
+
var h = (location.hash || "").slice(1);
|
|
451
|
+
return byId[h] ? h : "overview";
|
|
452
|
+
}
|
|
453
|
+
function time(iso) {
|
|
454
|
+
try {
|
|
455
|
+
return new Date(iso).toLocaleTimeString([], {
|
|
456
|
+
hour: "2-digit",
|
|
457
|
+
minute: "2-digit",
|
|
458
|
+
});
|
|
459
|
+
} catch (e) {
|
|
460
|
+
return iso;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function byline(c) {
|
|
464
|
+
return [c.authors, c.venue, c.year].filter(Boolean).join(" · ");
|
|
465
|
+
}
|
|
466
|
+
function canWrite() {
|
|
467
|
+
return db && state.store === "ok";
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function save(id, choice) {
|
|
471
|
+
var reason = (
|
|
472
|
+
state.drafts[id] !== undefined
|
|
473
|
+
? state.drafts[id]
|
|
474
|
+
: (decisionOf(id) || {}).reason || ""
|
|
475
|
+
).trim();
|
|
476
|
+
if (choice === "skip" && !reason) {
|
|
477
|
+
state.failed[id] =
|
|
478
|
+
"Skip needs a reason: type it above, then press Skip again.";
|
|
479
|
+
render();
|
|
480
|
+
var t = document.getElementById("reason-" + id);
|
|
481
|
+
if (t) t.focus();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
var doc = {
|
|
485
|
+
choice: choice,
|
|
486
|
+
reason: reason,
|
|
487
|
+
at: new Date().toISOString(),
|
|
488
|
+
source: "page",
|
|
489
|
+
};
|
|
490
|
+
state.pending[id] = true;
|
|
491
|
+
delete state.failed[id];
|
|
492
|
+
render();
|
|
493
|
+
db.collection("decisions")
|
|
494
|
+
.doc(id)
|
|
495
|
+
.set(doc)
|
|
496
|
+
.then(
|
|
497
|
+
function () {
|
|
498
|
+
state.saved[id] = doc;
|
|
499
|
+
delete state.pending[id];
|
|
500
|
+
// Only the text that was sent is now saved; a reason typed while the save was in flight stays a draft.
|
|
501
|
+
if (
|
|
502
|
+
state.drafts[id] !== undefined &&
|
|
503
|
+
state.drafts[id].trim() === reason
|
|
504
|
+
)
|
|
505
|
+
delete state.drafts[id];
|
|
506
|
+
render();
|
|
507
|
+
},
|
|
508
|
+
function (e) {
|
|
509
|
+
delete state.pending[id];
|
|
510
|
+
state.failed[id] =
|
|
511
|
+
"Not saved (" +
|
|
512
|
+
((e && e.code) || "store unavailable") +
|
|
513
|
+
"). Try again.";
|
|
514
|
+
render();
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderTabs() {
|
|
520
|
+
var tabs = document.getElementById("tabs");
|
|
521
|
+
tabs.textContent = "";
|
|
522
|
+
var cur = current();
|
|
523
|
+
var add = function (id, label, choice) {
|
|
524
|
+
var b = el("button", "tab");
|
|
525
|
+
b.type = "button";
|
|
526
|
+
b.setAttribute("role", "tab");
|
|
527
|
+
b.setAttribute("aria-selected", String(cur === id));
|
|
528
|
+
if (id !== "overview")
|
|
529
|
+
b.appendChild(el("span", "dot" + (choice ? " " + choice : "")));
|
|
530
|
+
b.appendChild(el("span", "", label));
|
|
531
|
+
b.addEventListener("click", function () {
|
|
532
|
+
location.hash = id;
|
|
533
|
+
});
|
|
534
|
+
tabs.appendChild(b);
|
|
535
|
+
};
|
|
536
|
+
add("overview", "Overview");
|
|
537
|
+
DATA.candidates.forEach(function (c) {
|
|
538
|
+
var d = decisionOf(c.id);
|
|
539
|
+
add(
|
|
540
|
+
c.id,
|
|
541
|
+
(c.authors || c.id).split(/[ ,]/)[0] + " " + (c.year || ""),
|
|
542
|
+
d && d.choice,
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function overview(view) {
|
|
548
|
+
var intro = section();
|
|
549
|
+
intro.appendChild(el("h2", "", "Works waiting for your call"));
|
|
550
|
+
intro.appendChild(
|
|
551
|
+
el(
|
|
552
|
+
"p",
|
|
553
|
+
"",
|
|
554
|
+
"Each tab is one work a refutation pass or a search turned up. Look at the highlighted passages, then " +
|
|
555
|
+
"choose: add it as a neighbour, cite it only, or skip it with a reason. A choice saves when you press it.",
|
|
556
|
+
),
|
|
557
|
+
);
|
|
558
|
+
view.appendChild(intro);
|
|
559
|
+
var s = section(),
|
|
560
|
+
box = el("div", "raised tablebox"),
|
|
561
|
+
t = el("table"),
|
|
562
|
+
head = el("tr");
|
|
563
|
+
["Work", "Came up for", "I suggest", "Your call"].forEach(function (h) {
|
|
564
|
+
head.appendChild(el("th", "", h));
|
|
565
|
+
});
|
|
566
|
+
t.appendChild(head);
|
|
567
|
+
DATA.candidates.forEach(function (c) {
|
|
568
|
+
var tr = el("tr"),
|
|
569
|
+
w = el("td"),
|
|
570
|
+
a = el("a", "", c.title);
|
|
571
|
+
a.href = "#" + c.id;
|
|
572
|
+
w.appendChild(a);
|
|
573
|
+
w.appendChild(el("div", "meta", byline(c)));
|
|
574
|
+
tr.appendChild(w);
|
|
575
|
+
tr.appendChild(
|
|
576
|
+
el(
|
|
577
|
+
"td",
|
|
578
|
+
"",
|
|
579
|
+
(c.found_by || [])
|
|
580
|
+
.map(function (f) {
|
|
581
|
+
return f.claim || "the paper's own references";
|
|
582
|
+
})
|
|
583
|
+
.join(", "),
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
tr.appendChild(el("td", "", c.suggestion));
|
|
587
|
+
var d = decisionOf(c.id);
|
|
588
|
+
tr.appendChild(el("td", "", d ? d.choice : "not decided"));
|
|
589
|
+
t.appendChild(tr);
|
|
590
|
+
});
|
|
591
|
+
box.appendChild(t);
|
|
592
|
+
s.appendChild(box);
|
|
593
|
+
view.appendChild(s);
|
|
594
|
+
var used = {};
|
|
595
|
+
DATA.candidates.forEach(function (c) {
|
|
596
|
+
c.passages.forEach(function (x) {
|
|
597
|
+
if (x.claim) used[x.claim] = true;
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
var ids = Object.keys(used)
|
|
601
|
+
.filter(function (k) {
|
|
602
|
+
return DATA.claims[k];
|
|
603
|
+
})
|
|
604
|
+
.sort();
|
|
605
|
+
if (ids.length) {
|
|
606
|
+
var cl = section("The claims these passages bear on");
|
|
607
|
+
ids.forEach(function (k) {
|
|
608
|
+
cl.appendChild(el("p", "claimtext", k + ": " + DATA.claims[k]));
|
|
609
|
+
});
|
|
610
|
+
view.appendChild(cl);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function candidate(view, c) {
|
|
615
|
+
var head = section();
|
|
616
|
+
head.appendChild(el("h2", "", c.title));
|
|
617
|
+
head.appendChild(el("p", "meta", byline(c)));
|
|
618
|
+
var links = el("div", "links");
|
|
619
|
+
if (c.pdf) {
|
|
620
|
+
var p = el("a", "", "Open the PDF");
|
|
621
|
+
p.href = c.pdf;
|
|
622
|
+
p.target = "_blank";
|
|
623
|
+
p.rel = "noopener";
|
|
624
|
+
links.appendChild(p);
|
|
625
|
+
}
|
|
626
|
+
if (c.url) {
|
|
627
|
+
var s = el("a", "", "Source page");
|
|
628
|
+
s.href = c.url;
|
|
629
|
+
s.target = "_blank";
|
|
630
|
+
s.rel = "noopener";
|
|
631
|
+
links.appendChild(s);
|
|
632
|
+
}
|
|
633
|
+
head.appendChild(links);
|
|
634
|
+
head.appendChild(el("p", "", c.summary));
|
|
635
|
+
view.appendChild(head);
|
|
636
|
+
|
|
637
|
+
// What the authors say about their own work, before any passage I picked (at the top).
|
|
638
|
+
var words = section("In the authors' words");
|
|
639
|
+
[
|
|
640
|
+
["Abstract", c.abstract],
|
|
641
|
+
["Conclusion", c.conclusion],
|
|
642
|
+
].forEach(function (part) {
|
|
643
|
+
var o = el("div", "own"),
|
|
644
|
+
paras = part[1].text.split(/\n\s*\n/).filter(function (t) {
|
|
645
|
+
return t.trim();
|
|
646
|
+
});
|
|
647
|
+
o.id = "own-" + c.id + "-" + part[0].toLowerCase();
|
|
648
|
+
o.appendChild(
|
|
649
|
+
el("figcaption", "", part[0] + " · PDF p. " + part[1].page),
|
|
650
|
+
);
|
|
651
|
+
o.appendChild(el("p", "", paras[0]));
|
|
652
|
+
if (paras.length > 1) {
|
|
653
|
+
var more = el("details");
|
|
654
|
+
more.appendChild(
|
|
655
|
+
el(
|
|
656
|
+
"summary",
|
|
657
|
+
"",
|
|
658
|
+
"The rest of the " +
|
|
659
|
+
part[0].toLowerCase() +
|
|
660
|
+
" (" +
|
|
661
|
+
(paras.length - 1) +
|
|
662
|
+
" more paragraph" +
|
|
663
|
+
(paras.length > 2 ? "s" : "") +
|
|
664
|
+
")",
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
paras.slice(1).forEach(function (t) {
|
|
668
|
+
more.appendChild(el("p", "", t));
|
|
669
|
+
});
|
|
670
|
+
o.appendChild(more);
|
|
671
|
+
}
|
|
672
|
+
words.appendChild(o);
|
|
673
|
+
});
|
|
674
|
+
view.appendChild(words);
|
|
675
|
+
|
|
676
|
+
var why = section("Why it came up");
|
|
677
|
+
(c.found_by || []).forEach(function (f) {
|
|
678
|
+
why.appendChild(
|
|
679
|
+
el(
|
|
680
|
+
"p",
|
|
681
|
+
"",
|
|
682
|
+
f.claim
|
|
683
|
+
? "The refutation pass for " +
|
|
684
|
+
f.claim +
|
|
685
|
+
" named it (" +
|
|
686
|
+
f.file.split("/").pop() +
|
|
687
|
+
")."
|
|
688
|
+
: f.source,
|
|
689
|
+
),
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
view.appendChild(why);
|
|
693
|
+
|
|
694
|
+
var ps = section("What it says, on the page");
|
|
695
|
+
c.passages.forEach(function (x) {
|
|
696
|
+
var f = el("figure", "passage");
|
|
697
|
+
var img = el("img");
|
|
698
|
+
img.src = x.img;
|
|
699
|
+
img.alt = "Page " + x.page + ", passage highlighted: " + x.quote;
|
|
700
|
+
img.loading = "lazy";
|
|
701
|
+
f.appendChild(img);
|
|
702
|
+
f.appendChild(
|
|
703
|
+
el(
|
|
704
|
+
"figcaption",
|
|
705
|
+
"",
|
|
706
|
+
"PDF p. " + x.page + (x.claim ? " · " + x.claim : ""),
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
f.appendChild(el("blockquote", "", x.quote));
|
|
710
|
+
f.appendChild(el("p", "", x.meaning));
|
|
711
|
+
ps.appendChild(f);
|
|
712
|
+
});
|
|
713
|
+
view.appendChild(ps);
|
|
714
|
+
|
|
715
|
+
var dec = section("Your call"),
|
|
716
|
+
box = el("div", "raised"),
|
|
717
|
+
d = decisionOf(c.id),
|
|
718
|
+
off = !canWrite() || state.pending[c.id];
|
|
719
|
+
box.appendChild(
|
|
720
|
+
el(
|
|
721
|
+
"p",
|
|
722
|
+
"",
|
|
723
|
+
"I suggest " +
|
|
724
|
+
c.suggestion +
|
|
725
|
+
(c.suggestion_why ? ": " + c.suggestion_why : "."),
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
var row = el("div", "choices");
|
|
729
|
+
CHOICES.forEach(function (ch) {
|
|
730
|
+
var b = el("button", "choice " + ch[0]);
|
|
731
|
+
b.type = "button";
|
|
732
|
+
b.id = "choice-" + c.id + "-" + ch[0];
|
|
733
|
+
b.setAttribute("aria-pressed", String(!!d && d.choice === ch[0]));
|
|
734
|
+
b.disabled = !!off;
|
|
735
|
+
b.appendChild(el("b", "", ch[1]));
|
|
736
|
+
b.appendChild(el("span", "", ch[2]));
|
|
737
|
+
b.addEventListener("click", function () {
|
|
738
|
+
save(c.id, ch[0]);
|
|
739
|
+
});
|
|
740
|
+
row.appendChild(b);
|
|
741
|
+
});
|
|
742
|
+
var lab = el("label", "", "Reason (needed for Skip)");
|
|
743
|
+
lab.htmlFor = "reason-" + c.id;
|
|
744
|
+
var ta = el("textarea");
|
|
745
|
+
ta.id = "reason-" + c.id;
|
|
746
|
+
ta.value =
|
|
747
|
+
state.drafts[c.id] !== undefined
|
|
748
|
+
? state.drafts[c.id]
|
|
749
|
+
: (d && d.reason) || "";
|
|
750
|
+
ta.disabled = !canWrite();
|
|
751
|
+
ta.addEventListener("input", function () {
|
|
752
|
+
state.drafts[c.id] = ta.value;
|
|
753
|
+
});
|
|
754
|
+
box.appendChild(lab);
|
|
755
|
+
box.appendChild(ta);
|
|
756
|
+
box.appendChild(row);
|
|
757
|
+
var foot = el("div", "row");
|
|
758
|
+
if (d) {
|
|
759
|
+
var up = el("button", "btn quiet", "Save the reason");
|
|
760
|
+
up.type = "button";
|
|
761
|
+
up.id = "save-reason-" + c.id;
|
|
762
|
+
up.disabled = !!off;
|
|
763
|
+
up.addEventListener("click", function () {
|
|
764
|
+
save(c.id, d.choice);
|
|
765
|
+
});
|
|
766
|
+
foot.appendChild(up);
|
|
767
|
+
}
|
|
768
|
+
foot.appendChild(
|
|
769
|
+
state.failed[c.id]
|
|
770
|
+
? el("span", "status warn", state.failed[c.id])
|
|
771
|
+
: el(
|
|
772
|
+
"span",
|
|
773
|
+
"status" + (d ? " good" : ""),
|
|
774
|
+
state.pending[c.id]
|
|
775
|
+
? "Saving…"
|
|
776
|
+
: d
|
|
777
|
+
? state.saved[c.id]
|
|
778
|
+
? "Saved " + time(d.at)
|
|
779
|
+
: "Recorded in the repo " + time(d.at)
|
|
780
|
+
: "Not decided yet",
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
box.appendChild(foot);
|
|
784
|
+
dec.appendChild(box);
|
|
785
|
+
view.appendChild(dec);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function render() {
|
|
789
|
+
document.getElementById("title").textContent =
|
|
790
|
+
"Neighbour candidates · " + DATA.paper;
|
|
791
|
+
var n = decided().length,
|
|
792
|
+
total = DATA.candidates.length;
|
|
793
|
+
document.getElementById("progress").textContent =
|
|
794
|
+
n + " of " + total + " decided";
|
|
795
|
+
var send = document.getElementById("send");
|
|
796
|
+
send.hidden = !state.mcp || !DATA.session;
|
|
797
|
+
// A decision still saving would reach Claude as the previous one, so Send waits for every write.
|
|
798
|
+
send.disabled = n === 0 || state.send === "sending" || saving();
|
|
799
|
+
var st = document.getElementById("sendstatus");
|
|
800
|
+
st.textContent =
|
|
801
|
+
state.send && state.send !== "sending"
|
|
802
|
+
? state.send
|
|
803
|
+
: state.send === "sending"
|
|
804
|
+
? "Sending…"
|
|
805
|
+
: state.mcp && DATA.session
|
|
806
|
+
? ""
|
|
807
|
+
: "When you are done, tell Claude in chat; your choices are already saved.";
|
|
808
|
+
st.hidden = !st.textContent;
|
|
809
|
+
renderTabs();
|
|
810
|
+
var view = document.getElementById("view");
|
|
811
|
+
view.textContent = "";
|
|
812
|
+
if (state.store === "none") {
|
|
813
|
+
view.appendChild(
|
|
814
|
+
el(
|
|
815
|
+
"div",
|
|
816
|
+
"banner",
|
|
817
|
+
"Choices cannot be saved in this view, so it is read-only. Open the page from its claude.ai link, signed in.",
|
|
818
|
+
),
|
|
819
|
+
);
|
|
820
|
+
} else if (state.store === "loading") {
|
|
821
|
+
view.appendChild(el("p", "status", "Loading saved choices…"));
|
|
822
|
+
}
|
|
823
|
+
var cur = current();
|
|
824
|
+
if (cur === "overview") overview(view);
|
|
825
|
+
else candidate(view, byId[cur]);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function saving() {
|
|
829
|
+
return Object.keys(state.pending).length > 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function sendToClaude() {
|
|
833
|
+
if (saving()) return;
|
|
834
|
+
var lines = DATA.candidates.map(function (c) {
|
|
835
|
+
var d = decisionOf(c.id);
|
|
836
|
+
return (
|
|
837
|
+
"- " +
|
|
838
|
+
c.id +
|
|
839
|
+
": " +
|
|
840
|
+
(d
|
|
841
|
+
? d.choice + (d.reason ? " (" + d.reason + ")" : "")
|
|
842
|
+
: "not decided")
|
|
843
|
+
);
|
|
844
|
+
});
|
|
845
|
+
var message =
|
|
846
|
+
"Neighbour candidates for " +
|
|
847
|
+
DATA.paper +
|
|
848
|
+
": choices are saved on the page " +
|
|
849
|
+
"(decisions collection). Read them with ArtifactData, export them and go on.\n" +
|
|
850
|
+
lines.join("\n");
|
|
851
|
+
state.send = "sending";
|
|
852
|
+
render();
|
|
853
|
+
state.mcp
|
|
854
|
+
.callTool("Claude Code Remote", "send_message", {
|
|
855
|
+
session_id: DATA.session,
|
|
856
|
+
message: message,
|
|
857
|
+
})
|
|
858
|
+
.then(
|
|
859
|
+
function () {
|
|
860
|
+
state.send =
|
|
861
|
+
"Sent to Claude at " + time(new Date().toISOString()) + ".";
|
|
862
|
+
render();
|
|
863
|
+
},
|
|
864
|
+
function (e) {
|
|
865
|
+
var code = (e && e.code) || "upstream_error";
|
|
866
|
+
var fix =
|
|
867
|
+
{
|
|
868
|
+
server_not_connected:
|
|
869
|
+
"Claude Code Remote is not connected for you: add it in claude.ai Settings, Connectors, or tell Claude in chat.",
|
|
870
|
+
needs_reauth:
|
|
871
|
+
"Reconnect Claude Code Remote in claude.ai Settings, Connectors, or tell Claude in chat.",
|
|
872
|
+
approval_required:
|
|
873
|
+
"The page needs your approval to message Claude; press the button again and allow it, or tell Claude in chat.",
|
|
874
|
+
not_in_manifest:
|
|
875
|
+
"Messaging Claude is turned off for this page; tell Claude in chat.",
|
|
876
|
+
}[code] ||
|
|
877
|
+
"Could not send (" +
|
|
878
|
+
code +
|
|
879
|
+
"). It may have arrived anyway; tell Claude in chat to be sure.";
|
|
880
|
+
state.send = fix;
|
|
881
|
+
render();
|
|
882
|
+
},
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
document.getElementById("send").addEventListener("click", sendToClaude);
|
|
886
|
+
window.addEventListener("hashchange", function () {
|
|
887
|
+
render();
|
|
888
|
+
window.scrollTo(0, 0);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
render();
|
|
892
|
+
if (!window.claude || typeof window.claude.use !== "function") {
|
|
893
|
+
state.store = "none";
|
|
894
|
+
render();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
window.claude.use("db").then(
|
|
898
|
+
function (store) {
|
|
899
|
+
if (!store) {
|
|
900
|
+
state.store = "none";
|
|
901
|
+
render();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
db = store;
|
|
905
|
+
db.collection("decisions").onSnapshot(
|
|
906
|
+
function (snap) {
|
|
907
|
+
var next = {};
|
|
908
|
+
snap.docs.forEach(function (d) {
|
|
909
|
+
next[d.id] = d.data();
|
|
910
|
+
});
|
|
911
|
+
state.saved = next;
|
|
912
|
+
state.store = "ok";
|
|
913
|
+
render();
|
|
914
|
+
},
|
|
915
|
+
function () {
|
|
916
|
+
state.store = "none";
|
|
917
|
+
render();
|
|
918
|
+
},
|
|
919
|
+
);
|
|
920
|
+
},
|
|
921
|
+
function () {
|
|
922
|
+
state.store = "none";
|
|
923
|
+
render();
|
|
924
|
+
},
|
|
925
|
+
);
|
|
926
|
+
window.claude.use("mcp").then(
|
|
927
|
+
function (m) {
|
|
928
|
+
state.mcp = m;
|
|
929
|
+
render();
|
|
930
|
+
},
|
|
931
|
+
function () {},
|
|
932
|
+
);
|
|
933
|
+
})();
|
|
934
|
+
</script>
|