papilot 0.7.0 → 0.8.1

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.
Files changed (75) hide show
  1. package/README.md +53 -100
  2. package/README.zh-CN.md +52 -97
  3. package/bin/doctor.mjs +55 -6
  4. package/bin/installation.d.mts +2 -0
  5. package/bin/installation.mjs +7 -1
  6. package/dist/assets/{_virtual_papilot-licenses-BWIsfrQh.js → _virtual_papilot-licenses-CMfCtEYQ.js} +54 -3
  7. package/dist/assets/index-BlLyIqT9.js +265 -0
  8. package/dist/assets/index-DVZtyZvu.css +1 -0
  9. package/dist/assets/latex-BarqJFtQ.svg +1 -0
  10. package/dist/assets/{math-BsEl3u6P.js → math-Bu3xEyrC.js} +1 -1
  11. package/dist/assets/pdf-view-PbkCuTZ-.js +598 -0
  12. package/dist/index.html +2 -2
  13. package/examples/papilot-paper/main.tex +1 -1
  14. package/examples/papilot-paper/preview/main.pdf +0 -0
  15. package/examples/papilot-paper/preview/main.synctex.gz +0 -0
  16. package/examples/papilot-paper/sections/02-using.tex +3 -1
  17. package/examples/papilot-paper/sections/09-agents.tex +8 -5
  18. package/package.json +18 -8
  19. package/skills/ai-review.md +18 -0
  20. package/skills/latex-warnings.md +1 -1
  21. package/src/conversation.ts +6 -0
  22. package/src/server/acp.ts +387 -0
  23. package/src/server/agents.ts +220 -43
  24. package/src/server/ai-review-routes.ts +72 -0
  25. package/src/server/ai-review.ts +38 -0
  26. package/src/server/app.ts +56 -14
  27. package/src/server/build-problems.ts +329 -0
  28. package/src/server/changes.ts +25 -2
  29. package/src/server/codex.ts +2 -1
  30. package/src/server/cursor.ts +153 -0
  31. package/src/server/desktop-demo.ts +38 -0
  32. package/src/server/latex-installation.ts +43 -0
  33. package/src/server/manuscript.ts +183 -23
  34. package/src/server/paths.ts +1 -1
  35. package/src/server/reconcile.ts +20 -0
  36. package/src/server/references.ts +70 -0
  37. package/src/server/review.ts +6 -5
  38. package/src/server/skills.ts +5 -8
  39. package/src/server/start.ts +19 -4
  40. package/src/server/store.ts +20 -2
  41. package/src/server/update-install.ts +53 -2
  42. package/src/server/update-worker.ts +6 -6
  43. package/src/server/updates.ts +49 -14
  44. package/src/server/workspace.ts +236 -41
  45. package/src/shared.ts +88 -9
  46. package/src/skill-icons.ts +18 -0
  47. package/src/skill-metadata.ts +45 -0
  48. package/vendor/latex-review/dist/web/assets/index-CXdnUjct.js +19 -0
  49. package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +1 -0
  50. package/vendor/latex-review/dist/web/index.html +2 -2
  51. package/vendor/latex-review/src/core/bib.ts +692 -0
  52. package/vendor/latex-review/src/core/changes.ts +39 -0
  53. package/vendor/latex-review/src/core/current-commit.ts +104 -0
  54. package/vendor/latex-review/src/core/delivery.ts +62 -16
  55. package/vendor/latex-review/src/core/git.ts +2 -1
  56. package/vendor/latex-review/src/core/publish.ts +7 -1
  57. package/vendor/latex-review/src/core/reconcile.ts +101 -0
  58. package/vendor/latex-review/src/core/review.ts +6 -2
  59. package/vendor/latex-review/src/core/store.ts +1 -0
  60. package/vendor/latex-review/src/core/tasks.ts +1 -1
  61. package/vendor/latex-review/src/core/verify.ts +784 -0
  62. package/vendor/latex-review/src/core/view.ts +204 -0
  63. package/vendor/latex-review/src/render/round.ts +11 -0
  64. package/vendor/latex-review/src/server.ts +77 -1
  65. package/vendor/latex-review/src/types.ts +130 -2
  66. package/vendor/latex-review/src/web/effect-preferences.ts +10 -0
  67. package/vendor/latex-review/src/web/references.tsx +499 -0
  68. package/vendor/latex-review/src/web/review.tsx +263 -133
  69. package/vendor/latex-review/src/web/style.css +285 -0
  70. package/vendor/latex-review/src/web/zh-CN.ts +79 -2
  71. package/dist/assets/index-CIJEYlP7.js +0 -252
  72. package/dist/assets/index-hOJEruFc.css +0 -1
  73. package/dist/assets/pdf-view-Cz9_vK5U.js +0 -598
  74. package/vendor/latex-review/dist/web/assets/index-Be9CtgcZ.css +0 -1
  75. package/vendor/latex-review/dist/web/assets/index-TvMWVSaw.js +0 -19
@@ -0,0 +1,499 @@
1
+ import { Fragment, useEffect, useRef, useState } from "react";
2
+ import type {
3
+ CheckDifference,
4
+ ReferenceCheck,
5
+ ReferenceChecks,
6
+ ReferenceCitation,
7
+ ReferenceEntry,
8
+ ReferenceProblem,
9
+ References,
10
+ RoundDetail,
11
+ } from "../types.ts";
12
+ import { language, t, tn } from "./i18n.ts";
13
+
14
+ // Bibliography changes are shown whole, entry by entry, with what an online
15
+ // check found. An entry that is not verified is accepted only after the
16
+ // author confirms it, and batch acceptance leaves it for the author.
17
+
18
+ /** The host can turn online checks off; a page that cannot read the
19
+ * setting checks online. */
20
+ export const REFERENCE_STORAGE = "latex-review:references";
21
+ export function onlineChecks() {
22
+ try {
23
+ return localStorage.getItem(REFERENCE_STORAGE) !== "off";
24
+ } catch {
25
+ return true;
26
+ }
27
+ }
28
+
29
+ const SOURCE: Record<string, string> = {
30
+ doi: "doi.org",
31
+ crossref: "Crossref",
32
+ dblp: "DBLP",
33
+ arxiv: "arXiv",
34
+ };
35
+ const FIELD: Record<CheckDifference["field"], string> = {
36
+ title: "Title",
37
+ author: "Authors",
38
+ year: "Year",
39
+ doi: "DOI",
40
+ arxiv: "arXiv",
41
+ };
42
+ const ENTRY_STATUS: Record<ReferenceEntry["status"], string> = {
43
+ added: "New entry",
44
+ modified: "Changed entry",
45
+ removed: "Removed entry",
46
+ };
47
+ const CHECK: Record<string, string> = {
48
+ verified: "Verified",
49
+ mismatch: "Differs from the record",
50
+ unverified: "Not verified",
51
+ failed: "Could not check",
52
+ skipped: "Nothing to check",
53
+ checking: "Checking…",
54
+ off: "Not checked online",
55
+ confirmed: "Checked by you",
56
+ };
57
+ const PROBLEM: Record<ReferenceProblem, string> = {
58
+ "still-cited": "The paper still cites this key, and no entry defines it now.",
59
+ "duplicate-key": "Another entry in the bibliography has the same key.",
60
+ unreadable: "BibTeX cannot read this entry.",
61
+ "not-in-bibliography": "The paper does not use this .bib file, so the entry will not appear.",
62
+ };
63
+
64
+ export interface CheckState {
65
+ round: number;
66
+ value: ReferenceChecks | null;
67
+ error: boolean;
68
+ }
69
+ /** Asks for the checks of a round's entries until none is pending. */
70
+ export function useReferenceChecks(
71
+ detail: RoundDetail | null,
72
+ load: <T>(route: string, body?: unknown) => Promise<T>,
73
+ ) {
74
+ const [state, setState] = useState<CheckState | null>(null),
75
+ retrying = useRef(false),
76
+ [attempt, setAttempt] = useState(0),
77
+ checked = Boolean(detail?.references?.entries.some((e) => e.status !== "removed" && !e.error));
78
+ useEffect(() => {
79
+ if (!detail || !checked) {
80
+ setState(null);
81
+ return;
82
+ }
83
+ let cancelled = false,
84
+ timer: ReturnType<typeof setTimeout> | undefined;
85
+ const round = detail.round;
86
+ async function poll() {
87
+ const online = onlineChecks(),
88
+ retry = retrying.current;
89
+ retrying.current = false;
90
+ try {
91
+ const value = await load<ReferenceChecks>(
92
+ `/rounds/${round}/references${online ? `?check=1${retry ? "&retry=1" : ""}` : ""}`,
93
+ );
94
+ if (cancelled) return;
95
+ setState({ round, value, error: false });
96
+ if (online && value.pending) timer = setTimeout(poll, document.hidden ? 5000 : 1500);
97
+ } catch {
98
+ if (cancelled) return;
99
+ setState((s) => ({ round, value: s?.round === round ? s.value : null, error: true }));
100
+ timer = setTimeout(poll, 5000);
101
+ }
102
+ }
103
+ void poll();
104
+ return () => {
105
+ cancelled = true;
106
+ clearTimeout(timer);
107
+ };
108
+ }, [detail?.round, detail?.after, checked, attempt]);
109
+ const current = state && detail && state.round === detail.round ? state : null;
110
+ return {
111
+ checks: current?.value?.checks ?? {},
112
+ pending: current?.value?.pending ?? (checked && onlineChecks() ? 1 : 0),
113
+ confirmed: current?.value?.confirmed ?? {},
114
+ error: Boolean(current?.error),
115
+ retry() {
116
+ retrying.current = true;
117
+ setAttempt((n) => n + 1);
118
+ },
119
+ async confirm(id: string, on: boolean) {
120
+ if (!detail) return;
121
+ const round = detail.round,
122
+ value = await load<ReferenceChecks>(`/rounds/${round}/references/confirm`, { id, confirmed: on });
123
+ setState((s) => (s?.round === round ? { round, value: { ...value, pending: Math.max(value.pending, s.value?.pending ?? 0) }, error: false } : s));
124
+ },
125
+ };
126
+ }
127
+
128
+ /** What the author should know before accepting an entry; empty when it is
129
+ * verified and nothing is wrong with it. */
130
+ export function concerns(
131
+ entry: ReferenceEntry,
132
+ check: ReferenceCheck | undefined,
133
+ pending: boolean,
134
+ confirmed = false,
135
+ ): string[] {
136
+ const found = entry.problems.map((p) => t(PROBLEM[p]));
137
+ if (entry.status === "removed" || entry.error || confirmed) return found;
138
+ const state = checkState(check, pending);
139
+ if (state !== "verified") found.push(t(CHECK[state]));
140
+ return found;
141
+ }
142
+ function checkState(check: ReferenceCheck | undefined, pending: boolean) {
143
+ if (check) return check.status;
144
+ return pending && onlineChecks() ? "checking" : "off";
145
+ }
146
+ /** Each change's entries that need a look before acceptance, and the
147
+ * citations it adds without an entry. */
148
+ export function risks(
149
+ references: References | undefined,
150
+ checks: Record<string, ReferenceCheck>,
151
+ pending: number,
152
+ confirmed: Record<string, number> = {},
153
+ ) {
154
+ const result = new Map<string, { label: string; reasons: string[] }[]>();
155
+ for (const entry of references?.entries ?? []) {
156
+ const reasons = concerns(entry, checks[entry.id], pending > 0, entry.id in confirmed);
157
+ if (reasons.length)
158
+ for (const unit of entry.units ?? [entry.unit])
159
+ result.set(unit, [...(result.get(unit) ?? []), { label: entry.key, reasons }]);
160
+ }
161
+ for (const cite of references?.missing ?? [])
162
+ if (cite.unit)
163
+ result.set(cite.unit, [
164
+ ...(result.get(cite.unit) ?? []),
165
+ { label: `\\cite{${cite.key}}`, reasons: [t("No bibliography entry has this key.")] },
166
+ ]);
167
+ return result;
168
+ }
169
+
170
+ // Chinese sentences follow each other without a space.
171
+ const gap = () => (language() === "zh-CN" ? "" : " ");
172
+ function where(place: ReferenceCitation) {
173
+ return `${place.section ? `${place.section} · ` : ""}${place.file}:${place.line}`;
174
+ }
175
+ function Cited({ entry }: { entry: ReferenceEntry }) {
176
+ const places = entry.status === "removed" ? [] : entry.cited;
177
+ if (entry.status === "removed") return null;
178
+ return (
179
+ <p className="reference-cited">
180
+ {places.length
181
+ ? tn(places.length, "Cited {n} time: {places}", "Cited {n} times: {places}", { places: places.map(where).join("; ") })
182
+ : t("Not cited in the paper, so it does not appear in the PDF.")}
183
+ </p>
184
+ );
185
+ }
186
+ function Value({ old, next, status }: { old: string | null; next: string | null; status: ReferenceEntry["status"] }) {
187
+ if (status !== "modified" || old === next) return <>{next ?? old}</>;
188
+ return (
189
+ <>
190
+ {old !== null && <del>{old}</del>}
191
+ {old !== null && next !== null && " "}
192
+ {next !== null && <ins>{next}</ins>}
193
+ </>
194
+ );
195
+ }
196
+ function Verdict({ check, state, entry, onRetry, confirmed, onConfirm }: {
197
+ check?: ReferenceCheck;
198
+ state: string;
199
+ entry: ReferenceEntry;
200
+ onRetry: () => void;
201
+ confirmed?: number;
202
+ onConfirm?: (on: boolean) => void;
203
+ }) {
204
+ if (entry.status === "removed" || entry.error) return null;
205
+ const confirmation =
206
+ onConfirm && (confirmed !== undefined || (state !== "verified" && state !== "checking")) ? (
207
+ <p className="reference-confirmation">
208
+ {confirmed !== undefined ? (
209
+ <>
210
+ {t("You checked this entry yourself.")}{" "}
211
+ <button type="button" className="link" onClick={() => onConfirm(false)}>
212
+ {t("Undo")}
213
+ </button>
214
+ </>
215
+ ) : (
216
+ <button type="button" className="reference-confirm" onClick={() => onConfirm(true)}>
217
+ {t("I checked it myself")}
218
+ </button>
219
+ )}
220
+ </p>
221
+ ) : null;
222
+ const source = check?.record ? SOURCE[check.record.source] : "",
223
+ unreachable = check?.unavailable.length
224
+ ? t("{sources} could not be reached.", { sources: check.unavailable.join(", ") })
225
+ : "";
226
+ const text =
227
+ state === "checking"
228
+ ? t("Looking this entry up in Crossref, DBLP and arXiv…")
229
+ : state === "off"
230
+ ? t("Online checks are off in Settings, so this entry was not looked up.")
231
+ : state === "verified"
232
+ ? check!.differences.length
233
+ ? t("Matches the record in {source}, apart from the small differences below.", { source })
234
+ : t("Matches the record in {source}.", { source })
235
+ : state === "mismatch"
236
+ ? t("{source} has this work with other details. Compare them below.", { source })
237
+ : state === "unverified"
238
+ ? check!.reason === "doi-not-found"
239
+ ? t("The DOI is not registered, and no record matches the title.")
240
+ : check!.reason === "arxiv-not-found"
241
+ ? t("No arXiv paper has this identifier, and no record matches the title.")
242
+ : t("No record in Crossref, DBLP or arXiv matches this entry. Theses, reports and many books are in none of them, so check it yourself.")
243
+ : state === "failed"
244
+ ? t("The sources could not be reached.")
245
+ : t("The entry has no title, DOI or arXiv identifier to look up.");
246
+ return (
247
+ <div className="reference-verdict" data-check={state}>
248
+ <p>
249
+ {text}
250
+ {unreachable && gap() + unreachable}
251
+ {(state === "failed" || state === "unverified" || (check?.unavailable.length ?? 0) > 0) && (
252
+ <>
253
+ {" "}
254
+ <button type="button" className="link" onClick={onRetry}>
255
+ {t("Check again")}
256
+ </button>
257
+ </>
258
+ )}
259
+ </p>
260
+ {check && check.differences.length > 0 && (
261
+ <dl className="reference-differences">
262
+ {check.differences.map((d, i) => (
263
+ <Fragment key={i}>
264
+ <dt>
265
+ {t(FIELD[d.field])}
266
+ {d.minor ? ` · ${t("small")}` : ""}
267
+ </dt>
268
+ <dd>
269
+ <span className="reference-ours">
270
+ <span className="reference-side">{t("Entry")}</span>
271
+ {d.bib}
272
+ </span>
273
+ <span className="reference-theirs">
274
+ <span className="reference-side">{SOURCE[check.record?.source ?? ""] ?? t("Record")}</span>
275
+ {d.record || (d.field === "doi" ? t("(not registered)") : d.field === "arxiv" ? t("(no such paper)") : "—")}
276
+ </span>
277
+ </dd>
278
+ </Fragment>
279
+ ))}
280
+ </dl>
281
+ )}
282
+ {check?.record && (
283
+ <p className="reference-record">
284
+ <span className="reference-side">
285
+ {state === "unverified" ? t("Closest record") : t("Record")} · {source}
286
+ </span>
287
+ <a href={check.record.url} target="_blank" rel="noopener noreferrer">
288
+ {check.record.title}
289
+ </a>
290
+ {[
291
+ check.record.authors.join(", "),
292
+ check.record.venue,
293
+ check.record.year,
294
+ check.record.doi ? `doi:${check.record.doi}` : null,
295
+ ]
296
+ .filter(Boolean)
297
+ .map((part, i) => (
298
+ <span key={i}> · {part}</span>
299
+ ))}
300
+ </p>
301
+ )}
302
+ {confirmation}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ /** A change's bibliography entries, every field shown. */
308
+ export function ReferenceEntries({ entries, checks, pending, onRetry, confirmed = {}, onConfirm }: {
309
+ entries: ReferenceEntry[];
310
+ checks: Record<string, ReferenceCheck>;
311
+ pending: boolean;
312
+ onRetry: () => void;
313
+ confirmed?: Record<string, number>;
314
+ onConfirm?: (id: string, on: boolean) => void;
315
+ }) {
316
+ return (
317
+ <div className="references">
318
+ {entries.map((entry) => {
319
+ const check = checks[entry.id],
320
+ state = entry.status === "removed" ? "removed" : entry.error ? "unreadable" : checkState(check, pending),
321
+ mine = confirmed[entry.id],
322
+ shown = mine !== undefined && state !== "verified" && state !== "removed" && state !== "unreadable" ? "confirmed" : state;
323
+ return (
324
+ <section className="reference-entry" key={entry.id} data-status={entry.status} data-check={shown}>
325
+ <header className="reference-head">
326
+ <code className="reference-key">
327
+ @{entry.old_type ? <><del>{entry.old_type}</del><ins>{entry.type}</ins></> : entry.type}
328
+ {"{"}
329
+ {entry.old_key ? <><del>{entry.old_key}</del><ins>{entry.key}</ins></> : entry.key}
330
+ {"}"}
331
+ </code>
332
+ <span className="reference-status">{t(ENTRY_STATUS[entry.status])}</span>
333
+ {state !== "removed" && state !== "unreadable" && (
334
+ <span className="reference-check" data-check={shown}>
335
+ {t(CHECK[shown])}
336
+ </span>
337
+ )}
338
+ <span className="reference-lines">
339
+ {entry.file}:{entry.lines[0]}–{entry.lines[1]}
340
+ </span>
341
+ </header>
342
+ {entry.problems.map((p) => (
343
+ <p className="reference-problem" key={p}>
344
+ {t(PROBLEM[p])}
345
+ {p === "unreadable" && entry.error ? gap() + entry.error : ""}
346
+ {p === "still-cited" &&
347
+ gap() + [...(entry.status === "removed" ? entry.cited : []), ...(entry.old_cited ?? [])].map(where).join("; ")}
348
+ </p>
349
+ ))}
350
+ <table className="reference-fields">
351
+ <tbody>
352
+ {entry.fields.map((field) => (
353
+ <tr
354
+ key={field.name}
355
+ data-change={
356
+ entry.status !== "modified" || (field.old === field.new && (field.oldResolved ?? field.old) === (field.resolved ?? field.new))
357
+ ? undefined
358
+ : field.old === null
359
+ ? "added"
360
+ : field.new === null
361
+ ? "removed"
362
+ : "changed"
363
+ }
364
+ >
365
+ <th scope="row">{field.name}</th>
366
+ <td>
367
+ <Value old={field.old} next={field.new} status={entry.status} />
368
+ {(field.resolved !== undefined || field.oldResolved !== undefined) && (
369
+ <span className="reference-resolved"> = <Value old={field.oldResolved ?? field.old} next={field.resolved ?? field.new} status={entry.status} /></span>
370
+ )}
371
+ </td>
372
+ </tr>
373
+ ))}
374
+ </tbody>
375
+ </table>
376
+ <Cited entry={entry} />
377
+ <Verdict
378
+ check={check}
379
+ state={state}
380
+ entry={entry}
381
+ onRetry={onRetry}
382
+ confirmed={mine}
383
+ onConfirm={onConfirm && ((on) => onConfirm(entry.id, on))}
384
+ />
385
+ </section>
386
+ );
387
+ })}
388
+ </div>
389
+ );
390
+ }
391
+
392
+ /** One line above the document: how many entries changed, how many need a
393
+ * look, and the citations without an entry. */
394
+ export function ReferenceBanner({ references, checks, pending, confirmed = {}, changeOf, onSelect, onRetry }: {
395
+ references: References;
396
+ checks: Record<string, ReferenceCheck>;
397
+ pending: number;
398
+ confirmed?: Record<string, number>;
399
+ changeOf: (unit: string | null) => number;
400
+ onSelect: (index: number) => void;
401
+ onRetry: () => void;
402
+ }) {
403
+ const entries = references.entries,
404
+ looked = entries.filter((e) => e.status !== "removed" && !e.error),
405
+ mine = (e: ReferenceEntry) => e.id in confirmed && checks[e.id]?.status !== "verified",
406
+ count = (status: string) => looked.filter((e) => checks[e.id]?.status === status && !mine(e)).length,
407
+ flagged = entries
408
+ .map((entry) => ({ entry, reasons: concerns(entry, checks[entry.id], pending > 0, entry.id in confirmed) }))
409
+ .filter((item) => item.reasons.length),
410
+ summary = [
411
+ entries.length
412
+ ? tn(entries.length, "{n} entry changed", "{n} entries changed")
413
+ : tn(references.missing.length, "{n} citation without an entry", "{n} citations without an entry"),
414
+ count("verified") && tn(count("verified"), "{n} verified", "{n} verified"),
415
+ looked.filter(mine).length && tn(looked.filter(mine).length, "{n} checked by you", "{n} checked by you"),
416
+ count("mismatch") && tn(count("mismatch"), "{n} differs from its record", "{n} differ from their records"),
417
+ count("unverified") && tn(count("unverified"), "{n} not verified", "{n} not verified"),
418
+ count("failed") && tn(count("failed"), "{n} could not be checked", "{n} could not be checked"),
419
+ pending > 0 && onlineChecks() && t("checking {n}…", { n: pending }),
420
+ !onlineChecks() && looked.length > 0 && t("online checks are off"),
421
+ ].filter(Boolean);
422
+ const link = (unit: string | null, label: string) => {
423
+ const index = changeOf(unit);
424
+ return index >= 0 ? (
425
+ <button type="button" className="link" onClick={() => onSelect(index)}>
426
+ {label}
427
+ </button>
428
+ ) : (
429
+ <span>{label}</span>
430
+ );
431
+ };
432
+ return (
433
+ <div className="banner references-banner" role="status">
434
+ <details open={flagged.length > 0 || references.missing.length > 0}>
435
+ <summary>
436
+ <strong>{t("Bibliography")}</strong> · {summary.join(" · ")}
437
+ </summary>
438
+ {(flagged.length > 0 || references.missing.length > 0) && (
439
+ <ul className="references-flagged">
440
+ {flagged.map(({ entry, reasons }) => (
441
+ <li key={entry.id}>
442
+ {link(entry.unit, entry.key)} — {reasons.join(gap())}
443
+ </li>
444
+ ))}
445
+ {references.missing.map((cite, i) => (
446
+ <li key={`missing-${i}`}>
447
+ {link(cite.unit, `\\cite{${cite.key}}`)} — {t("No bibliography entry has this key.")}{" "}
448
+ <span className="muted">{where(cite)}</span>
449
+ </li>
450
+ ))}
451
+ </ul>
452
+ )}
453
+ {count("failed") > 0 && (
454
+ <button type="button" className="link" onClick={onRetry}>
455
+ {t("Check again")}
456
+ </button>
457
+ )}
458
+ </details>
459
+ </div>
460
+ );
461
+ }
462
+
463
+ /** Asks before accepting entries that are not verified. */
464
+ export function ConfirmReferences({ items, staging, onCancel, onConfirm }: {
465
+ items: { label: string; reasons: string[] }[];
466
+ staging: boolean;
467
+ onCancel: () => void;
468
+ onConfirm: () => void;
469
+ }) {
470
+ const confirm = useRef<HTMLButtonElement>(null);
471
+ useEffect(() => confirm.current?.focus(), []);
472
+ return (
473
+ <div className="help-backdrop" onClick={onCancel}>
474
+ <dialog open aria-labelledby="references-confirm-title" className="references-confirm" onClick={(e) => e.stopPropagation()}>
475
+ <h2 id="references-confirm-title">
476
+ {staging ? t("Stage bibliography entries that are not verified?") : t("Accept bibliography entries that are not verified?")}
477
+ </h2>
478
+ <ul>
479
+ {items.map((item, i) => (
480
+ <li key={i}>
481
+ <code>{item.label}</code> — {item.reasons.join(gap())}
482
+ </li>
483
+ ))}
484
+ </ul>
485
+ <p className="muted">
486
+ {t("An agent can write an entry for a work that does not exist, or with the wrong authors, year or DOI. Check each one before it goes into your paper.")}
487
+ </p>
488
+ <div className="references-confirm-actions">
489
+ <button type="button" onClick={onCancel}>
490
+ {t("Cancel")}
491
+ </button>
492
+ <button type="button" className="accept" ref={confirm} onClick={onConfirm}>
493
+ {staging ? t("Stage anyway") : t("Accept anyway")}
494
+ </button>
495
+ </div>
496
+ </dialog>
497
+ </div>
498
+ );
499
+ }