papilot 0.6.0 → 0.8.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/README.md +58 -30
- package/README.zh-CN.md +58 -30
- package/bin/doctor.mjs +53 -6
- package/bin/installation.d.mts +8 -0
- package/bin/installation.mjs +54 -0
- package/bin/papilot.mjs +6 -2
- package/dist/assets/_virtual_papilot-licenses-D7ro6ecN.js +7597 -0
- package/dist/assets/index-Cc-awdNn.css +1 -0
- package/dist/assets/index-D_VX51no.js +265 -0
- package/dist/assets/latex-BarqJFtQ.svg +1 -0
- package/dist/assets/{math-DY-H5SfX.js → math-V9uUaUzA.js} +1 -1
- package/dist/assets/pdf-view-Bnb2uIBE.js +598 -0
- package/dist/index.html +2 -2
- package/examples/papilot-paper/main.tex +1 -1
- package/examples/papilot-paper/sections/02-using.tex +3 -1
- package/examples/papilot-paper/sections/09-agents.tex +8 -5
- package/package.json +15 -7
- package/scripts/service.ts +1 -0
- package/skills/ai-review.md +18 -0
- package/skills/latex-warnings.md +1 -1
- package/src/conversation.ts +6 -0
- package/src/note-groups.ts +30 -0
- package/src/notes.ts +34 -10
- package/src/server/acp.ts +354 -0
- package/src/server/agents.ts +237 -47
- package/src/server/ai-review-routes.ts +72 -0
- package/src/server/ai-review.ts +38 -0
- package/src/server/annotation-placement.ts +99 -0
- package/src/server/app.ts +216 -21
- package/src/server/attachments.ts +128 -0
- package/src/server/build-problems.ts +329 -0
- package/src/server/changes.ts +28 -2
- package/src/server/codex.ts +2 -1
- package/src/server/cursor.ts +153 -0
- package/src/server/diagnostics.ts +45 -0
- package/src/server/git-overview.ts +69 -0
- package/src/server/imports.ts +205 -0
- package/src/server/latex-installation.ts +43 -0
- package/src/server/main.ts +8 -42
- package/src/server/manuscript.ts +212 -12
- package/src/server/note-results.ts +65 -0
- package/src/server/paths.ts +1 -1
- package/src/server/references.ts +70 -0
- package/src/server/review.ts +6 -5
- package/src/server/skill-library.ts +113 -0
- package/src/server/skill-routes.ts +19 -0
- package/src/server/skills.ts +29 -19
- package/src/server/start.ts +51 -0
- package/src/server/store.ts +44 -4
- package/src/server/update-install.ts +124 -0
- package/src/server/update-worker.ts +78 -0
- package/src/server/updates.ts +152 -0
- package/src/server/workspace.ts +450 -60
- package/src/shared.ts +120 -10
- package/src/skill-icons.ts +18 -0
- package/src/skill-metadata.ts +45 -0
- package/src/task-caption.ts +20 -0
- package/vendor/latex-review/dist/web/assets/index-7VKAte5x.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/bib.ts +692 -0
- package/vendor/latex-review/src/core/build.ts +5 -1
- package/vendor/latex-review/src/core/changes.ts +39 -0
- package/vendor/latex-review/src/core/delivery.ts +109 -0
- package/vendor/latex-review/src/core/review.ts +12 -4
- package/vendor/latex-review/src/core/store.ts +3 -0
- package/vendor/latex-review/src/core/tasks.ts +5 -2
- package/vendor/latex-review/src/core/verify.ts +784 -0
- package/vendor/latex-review/src/core/view.ts +204 -0
- package/vendor/latex-review/src/render/round.ts +11 -0
- package/vendor/latex-review/src/server.ts +81 -1
- package/vendor/latex-review/src/types.ts +141 -0
- package/vendor/latex-review/src/web/effect-preferences.ts +10 -0
- package/vendor/latex-review/src/web/i18n.ts +1 -1
- package/vendor/latex-review/src/web/overview.tsx +1 -5
- package/vendor/latex-review/src/web/references.tsx +499 -0
- package/vendor/latex-review/src/web/review.tsx +381 -230
- package/vendor/latex-review/src/web/shortcuts.ts +53 -0
- package/vendor/latex-review/src/web/style.css +295 -0
- package/vendor/latex-review/src/web/zh-CN.ts +95 -26
- package/dist/assets/index-BNGqA0_L.css +0 -1
- package/dist/assets/index-Bt9UXsGX.js +0 -222
- package/dist/assets/pdf-view-BGp3W-RG.js +0 -598
- package/vendor/latex-review/dist/web/assets/index-AFBj4gQ9.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-DfC0GURl.css +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { Builder, BuildError } from "./build.ts";
|
|
5
|
+
import { cleanPath, within } from "./git.ts";
|
|
6
|
+
import { run } from "./process.ts";
|
|
7
|
+
import { mupdf } from "../render/pdf.ts";
|
|
8
|
+
import type { App } from "../server.ts";
|
|
9
|
+
import type { BuildInfo } from "../types.ts";
|
|
10
|
+
// A task's agent looks at its typeset pages: the task worktree as it is now,
|
|
11
|
+
// or the accepted text, compiled like a review round (only the chapters the
|
|
12
|
+
// edits and the asked-for places need, when the paper allows partial builds,
|
|
13
|
+
// so a later publication of the same text reuses the build) and drawn as PNG
|
|
14
|
+
// files the agent opens with its own image viewer. Places are source lines
|
|
15
|
+
// or printed page numbers.
|
|
16
|
+
export interface ViewRequest {
|
|
17
|
+
locations?: string[];
|
|
18
|
+
pages?: string[];
|
|
19
|
+
accepted?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ViewedPage {
|
|
22
|
+
/** The page's position in the PDF that was built, from 1. */
|
|
23
|
+
index: number;
|
|
24
|
+
/** The PDF's page label, or null when no numbering metadata is available. */
|
|
25
|
+
label: string | null;
|
|
26
|
+
/** What asked for this page. */
|
|
27
|
+
shows: string[];
|
|
28
|
+
/** The drawn page. */
|
|
29
|
+
file: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ViewResult {
|
|
32
|
+
pages: ViewedPage[];
|
|
33
|
+
/** Requests no page answered, with the reason. */
|
|
34
|
+
missing: string[];
|
|
35
|
+
targets: string[];
|
|
36
|
+
error?: string;
|
|
37
|
+
details?: string[];
|
|
38
|
+
}
|
|
39
|
+
// Pages drawn per request, and how far from an asked-for line SyncTeX may
|
|
40
|
+
// look for one that printed something (blank lines and comments print nothing).
|
|
41
|
+
const PAGES = 4,
|
|
42
|
+
NEARBY = 10,
|
|
43
|
+
SCALE = 1.5,
|
|
44
|
+
// Drawn pages are kept this long, for an agent that opens them later in its turn.
|
|
45
|
+
KEEP = 6 * 3600 * 1000;
|
|
46
|
+
// A new folder for one view's pages, after removing the task's old ones.
|
|
47
|
+
function folder(app: App, name: string) {
|
|
48
|
+
const views = path.join(app.project.state, "views", name);
|
|
49
|
+
for (const entry of fs.existsSync(views) ? fs.readdirSync(views) : []) {
|
|
50
|
+
const old = path.join(views, entry);
|
|
51
|
+
if (Date.now() - fs.statSync(old).mtimeMs > KEEP)
|
|
52
|
+
fs.rmSync(old, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
const created = path.join(views, `${Date.now()}-${randomBytes(3).toString("hex")}`);
|
|
55
|
+
fs.mkdirSync(created, { recursive: true });
|
|
56
|
+
return created;
|
|
57
|
+
}
|
|
58
|
+
interface Location {
|
|
59
|
+
asked: string;
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
}
|
|
63
|
+
function location(app: App, worktree: string, value: string): Location {
|
|
64
|
+
const match = /^(.*?):(\d+)$/.exec(value.trim());
|
|
65
|
+
if (!match) throw new Error(`Give a location as file:line, such as main.tex:12: ${value}`);
|
|
66
|
+
let file = match[1];
|
|
67
|
+
if (path.isAbsolute(file)) file = path.relative(worktree, file);
|
|
68
|
+
file = cleanPath(file);
|
|
69
|
+
if (!within(file, [app.project.source_dir]))
|
|
70
|
+
throw new Error(`${file} is outside the paper's source folder ${app.project.source_dir}.`);
|
|
71
|
+
return { asked: value.trim(), file, line: Number(match[2]) };
|
|
72
|
+
}
|
|
73
|
+
// The pages SyncTeX gives for a source line, or for the nearest line that
|
|
74
|
+
// printed something.
|
|
75
|
+
function pagesOf(pdf: string, file: string, line: number) {
|
|
76
|
+
for (let distance = 0; distance <= NEARBY; distance++)
|
|
77
|
+
for (const candidate of distance ? [line - distance, line + distance] : [line]) {
|
|
78
|
+
if (candidate < 1) continue;
|
|
79
|
+
const output = run(
|
|
80
|
+
"synctex",
|
|
81
|
+
["view", "-i", `${candidate}:0:${file}`, "-o", pdf],
|
|
82
|
+
{ check: false, timeout: 20000 },
|
|
83
|
+
).stdout.toString();
|
|
84
|
+
const pages = [
|
|
85
|
+
...new Set(
|
|
86
|
+
[...output.matchAll(/^Page:(\d+)$/gm)].map((m) => Number(m[1])),
|
|
87
|
+
),
|
|
88
|
+
];
|
|
89
|
+
if (pages.length) return pages;
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
export async function view(
|
|
94
|
+
app: App,
|
|
95
|
+
name: string,
|
|
96
|
+
request: ViewRequest,
|
|
97
|
+
): Promise<ViewResult> {
|
|
98
|
+
const task = app.tasks.get(name),
|
|
99
|
+
project = app.project;
|
|
100
|
+
if (!fs.existsSync(task.worktree))
|
|
101
|
+
throw new Error(`The task worktree ${task.worktree} no longer exists.`);
|
|
102
|
+
const locations = (request.locations ?? []).map((l) =>
|
|
103
|
+
location(app, task.worktree, l),
|
|
104
|
+
),
|
|
105
|
+
labels = (request.pages ?? []).map((p) => p.trim()).filter(Boolean);
|
|
106
|
+
if (!locations.length && !labels.length)
|
|
107
|
+
throw new Error("Ask for at least one source location or page number.");
|
|
108
|
+
const after = app.tasks.snapshot(task),
|
|
109
|
+
before = task.accepted,
|
|
110
|
+
builder = new Builder(project, app.repo),
|
|
111
|
+
targets = builder.plan(before, after, [
|
|
112
|
+
...new Set([
|
|
113
|
+
...app.repo.changedFiles(before, after),
|
|
114
|
+
...locations.map((l) => l.file),
|
|
115
|
+
]),
|
|
116
|
+
]);
|
|
117
|
+
let build: BuildInfo;
|
|
118
|
+
try {
|
|
119
|
+
const seed = targets[0] !== "full" ? await builder.build(task.base) : null;
|
|
120
|
+
build = await builder.build(request.accepted ? before : after, targets, seed);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof BuildError)) throw error;
|
|
123
|
+
return {
|
|
124
|
+
pages: [],
|
|
125
|
+
missing: [],
|
|
126
|
+
targets,
|
|
127
|
+
error: request.accepted
|
|
128
|
+
? "The accepted text could not be compiled."
|
|
129
|
+
: "The paper in the task worktree could not be compiled.",
|
|
130
|
+
details: error.details,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const pdf = build.pdf,
|
|
134
|
+
document = mupdf.Document.openDocument(fs.readFileSync(pdf), "application/pdf");
|
|
135
|
+
try {
|
|
136
|
+
// MuPDF synthesizes 1, 2, ... when /PageLabels is absent. Those are
|
|
137
|
+
// physical positions, not the printed numbers (front matter may be i, ii).
|
|
138
|
+
const trailer = document.asPDF()?.getTrailer(),
|
|
139
|
+
pageLabels = trailer?.get("Root", "PageLabels"),
|
|
140
|
+
hasLabels = pageLabels?.isDictionary() ?? false;
|
|
141
|
+
pageLabels?.destroy();
|
|
142
|
+
trailer?.destroy();
|
|
143
|
+
const wanted = new Map<number, string[]>(),
|
|
144
|
+
missing: string[] = [],
|
|
145
|
+
ask = (index: number, asked: string) =>
|
|
146
|
+
wanted.set(index, [...(wanted.get(index) ?? []), asked]);
|
|
147
|
+
for (const l of locations) {
|
|
148
|
+
// The build compiles an export of the source folder.
|
|
149
|
+
const file = path.join(
|
|
150
|
+
build.root,
|
|
151
|
+
path.posix.relative(project.source_dir || ".", l.file),
|
|
152
|
+
),
|
|
153
|
+
found = fs.existsSync(file) ? pagesOf(pdf, file, l.line) : [];
|
|
154
|
+
if (found.length) found.forEach((page) => ask(page, l.asked));
|
|
155
|
+
else missing.push(`${l.asked}: SyncTeX places no typeset text at or near this line.`);
|
|
156
|
+
}
|
|
157
|
+
if (labels.length && !hasLabels) {
|
|
158
|
+
missing.push("This PDF has no page-label metadata, so printed page numbers cannot be located reliably. Ask for source locations (file:line) instead.");
|
|
159
|
+
} else if (labels.length) {
|
|
160
|
+
const printed = Array.from({ length: document.countPages() }, (_, i) => {
|
|
161
|
+
const page = document.loadPage(i),
|
|
162
|
+
label = page.getLabel();
|
|
163
|
+
page.destroy();
|
|
164
|
+
return label;
|
|
165
|
+
});
|
|
166
|
+
for (const label of labels) {
|
|
167
|
+
const index = printed.findIndex(
|
|
168
|
+
(p) => p.toLowerCase() === label.toLowerCase(),
|
|
169
|
+
);
|
|
170
|
+
if (index >= 0) ask(index + 1, label);
|
|
171
|
+
else
|
|
172
|
+
missing.push(
|
|
173
|
+
targets[0] === "full"
|
|
174
|
+
? `Page ${label}: no page has this printed number.`
|
|
175
|
+
: `Page ${label}: not in the chapters compiled for this view (${targets.join(", ")}). Ask for a source line on that page instead.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const indices = [...wanted.keys()].sort((a, b) => a - b);
|
|
180
|
+
if (indices.length > PAGES)
|
|
181
|
+
missing.push(
|
|
182
|
+
`Only the first ${PAGES} pages are shown; ask for more specific source locations to see the remaining pages (physical positions ${indices.slice(PAGES).join(", ")} in this build).`,
|
|
183
|
+
);
|
|
184
|
+
const out = indices.length ? folder(app, name) : "";
|
|
185
|
+
const pages = indices.slice(0, PAGES).map((index) => {
|
|
186
|
+
const page = document.loadPage(index - 1),
|
|
187
|
+
pixmap = page.toPixmap(
|
|
188
|
+
[SCALE, 0, 0, SCALE, 0, 0],
|
|
189
|
+
mupdf.ColorSpace.DeviceRGB,
|
|
190
|
+
false,
|
|
191
|
+
true,
|
|
192
|
+
),
|
|
193
|
+
label = hasLabels ? page.getLabel() || null : null,
|
|
194
|
+
file = path.join(out, `page-${label && /^[\w-]{1,16}$/u.test(label) ? label : index}-${index}.png`);
|
|
195
|
+
fs.writeFileSync(file, pixmap.asPNG());
|
|
196
|
+
pixmap.destroy();
|
|
197
|
+
page.destroy();
|
|
198
|
+
return { index, label, shows: wanted.get(index)!, file };
|
|
199
|
+
});
|
|
200
|
+
return { pages, missing, targets };
|
|
201
|
+
} finally {
|
|
202
|
+
document.destroy();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { readJson, writeJson } from "../core/files.ts";
|
|
4
4
|
import { within } from "../core/git.ts";
|
|
5
5
|
import { deepestNode } from "../core/outline.ts";
|
|
6
|
+
import { referenceChanges } from "../core/bib.ts";
|
|
6
7
|
import { changesOf, placesOf, renderPlace } from "./effects.ts";
|
|
7
8
|
import { Build } from "./locate.ts";
|
|
8
9
|
import { renderSection, snap, Strips } from "./inline.ts";
|
|
@@ -54,8 +55,11 @@ export function classify(
|
|
|
54
55
|
/\\(?:verb\b|lstinline\b|mintinline\b|begin\{(?:verbatim\*?|Verbatim|lstlisting|minted)\})/.test(
|
|
55
56
|
(old.read(name) ?? "") + (next.read(name) ?? ""),
|
|
56
57
|
);
|
|
58
|
+
// A .bib file is not typeset where it is written, and % in a URL is not
|
|
59
|
+
// a comment there: its edits are compared in the PDF.
|
|
57
60
|
if (
|
|
58
61
|
unit.kind !== "binary" &&
|
|
62
|
+
!name.endsWith(".bib") &&
|
|
59
63
|
!whitespaceSensitive &&
|
|
60
64
|
!unit.truncated &&
|
|
61
65
|
unit.status === "modified" &&
|
|
@@ -286,6 +290,13 @@ export function renderRound(
|
|
|
286
290
|
if (!placed.has(index))
|
|
287
291
|
result.loose.push(looseBlock(card, strips, old, next));
|
|
288
292
|
});
|
|
293
|
+
try {
|
|
294
|
+
const references = referenceChanges(request.units, old, next);
|
|
295
|
+
if (references) result.references = references;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// The PDF review stands without the bibliography's entry view.
|
|
298
|
+
console.error("Could not read the bibliography changes:", error);
|
|
299
|
+
}
|
|
289
300
|
fs.writeFileSync(path.join(out, "round.json"), JSON.stringify(result));
|
|
290
301
|
return result;
|
|
291
302
|
} finally {
|
|
@@ -10,9 +10,12 @@ import { CONFIG_NAME, loadProject, type Project } from "./core/config.ts";
|
|
|
10
10
|
import { Store } from "./core/store.ts";
|
|
11
11
|
import { Tasks } from "./core/tasks.ts";
|
|
12
12
|
import { Review } from "./core/review.ts";
|
|
13
|
+
import { Deliveries } from "./core/delivery.ts";
|
|
13
14
|
import { more, PublishError, Publisher } from "./core/publish.ts";
|
|
15
|
+
import { view, type ViewRequest } from "./core/view.ts";
|
|
14
16
|
import { acquireLock, now, readJson, writeJson } from "./core/files.ts";
|
|
15
17
|
import { stopAll } from "./core/process.ts";
|
|
18
|
+
import { checkInputs, Confirmations, ReferenceChecker } from "./core/verify.ts";
|
|
16
19
|
import type { ActionKind } from "./types.ts";
|
|
17
20
|
export class App {
|
|
18
21
|
root: string;
|
|
@@ -25,7 +28,10 @@ export class App {
|
|
|
25
28
|
activity = now();
|
|
26
29
|
tasks: Tasks;
|
|
27
30
|
review: Review;
|
|
31
|
+
delivery: Deliveries;
|
|
28
32
|
publisher: Publisher;
|
|
33
|
+
references: ReferenceChecker;
|
|
34
|
+
confirmations: Confirmations;
|
|
29
35
|
private cachedProject?: Project;
|
|
30
36
|
private stamp?: number;
|
|
31
37
|
constructor(root: string) {
|
|
@@ -35,8 +41,16 @@ export class App {
|
|
|
35
41
|
this.store = new Store(path.join(this.state, "state.sqlite3"));
|
|
36
42
|
this.tasks = new Tasks(this);
|
|
37
43
|
this.review = new Review(this);
|
|
44
|
+
this.delivery = new Deliveries(this);
|
|
38
45
|
this.publisher = new Publisher(this);
|
|
46
|
+
this.references = new ReferenceChecker(
|
|
47
|
+
path.join(this.state, "references.json"),
|
|
48
|
+
);
|
|
49
|
+
this.confirmations = new Confirmations(
|
|
50
|
+
path.join(this.state, "references-confirmed.json"),
|
|
51
|
+
);
|
|
39
52
|
this.review.recover();
|
|
53
|
+
this.delivery.recover();
|
|
40
54
|
}
|
|
41
55
|
get project() {
|
|
42
56
|
const config = path.join(this.root, CONFIG_NAME),
|
|
@@ -52,9 +66,12 @@ export class App {
|
|
|
52
66
|
}
|
|
53
67
|
taskInfo(name: string) {
|
|
54
68
|
const task = this.tasks.get(name),
|
|
69
|
+
delivery = this.delivery.get(name),
|
|
55
70
|
scope = task.scope;
|
|
56
71
|
return {
|
|
57
72
|
...this.review.summary(name),
|
|
73
|
+
deferred: Boolean(task.deferred),
|
|
74
|
+
delivery: delivery ? { ...this.delivery.result(delivery), pending: !delivery.done } : null,
|
|
58
75
|
worktree: task.worktree,
|
|
59
76
|
url: this.page(name),
|
|
60
77
|
viewing: now() - (this.viewers.get(name) ?? 0) < 15,
|
|
@@ -203,6 +220,55 @@ export function createServer(app: App) {
|
|
|
203
220
|
}
|
|
204
221
|
},
|
|
205
222
|
);
|
|
223
|
+
// Checks of the bibliography entries a round adds or modifies. The page
|
|
224
|
+
// asks with check=1 to look up entries online, and keeps asking until
|
|
225
|
+
// none is pending; without it only results already known are returned.
|
|
226
|
+
server.get<{
|
|
227
|
+
Params: { name: string; number: string };
|
|
228
|
+
Querystring: { check?: string; retry?: string };
|
|
229
|
+
}>("/api/tasks/:name/rounds/:number/references", (request) => {
|
|
230
|
+
const inputs = checkInputs(
|
|
231
|
+
app.review.roundDetail(request.params.name, Number(request.params.number))
|
|
232
|
+
.references,
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
...app.references.lookup(
|
|
236
|
+
inputs,
|
|
237
|
+
request.query.check === "1",
|
|
238
|
+
request.query.retry === "1",
|
|
239
|
+
),
|
|
240
|
+
confirmed: app.confirmations.of(inputs),
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
// The author confirms, or takes back, that they checked an entry by hand.
|
|
244
|
+
server.post<{
|
|
245
|
+
Params: { name: string; number: string };
|
|
246
|
+
Body: { id: string; confirmed: boolean };
|
|
247
|
+
}>(
|
|
248
|
+
"/api/tasks/:name/rounds/:number/references/confirm",
|
|
249
|
+
{
|
|
250
|
+
schema: {
|
|
251
|
+
body: {
|
|
252
|
+
...{ type: "object" as const },
|
|
253
|
+
required: ["id", "confirmed"],
|
|
254
|
+
properties: { id: { type: "string" }, confirmed: { type: "boolean" } },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
(request, reply) => {
|
|
259
|
+
const inputs = checkInputs(
|
|
260
|
+
app.review.roundDetail(request.params.name, Number(request.params.number))
|
|
261
|
+
.references,
|
|
262
|
+
),
|
|
263
|
+
input = inputs.find((i) => i.id === request.body.id);
|
|
264
|
+
if (!input) return reply.code(404).send({ error: "No such entry in this round." });
|
|
265
|
+
app.confirmations.set(input, request.body.confirmed);
|
|
266
|
+
return {
|
|
267
|
+
...app.references.lookup(inputs),
|
|
268
|
+
confirmed: app.confirmations.of(inputs),
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
);
|
|
206
272
|
server.get<{ Params: { name: string; number: string; file: string } }>(
|
|
207
273
|
"/files/:name/:number/:file",
|
|
208
274
|
(request, reply) => {
|
|
@@ -232,6 +298,7 @@ export function createServer(app: App) {
|
|
|
232
298
|
title?: string;
|
|
233
299
|
sections?: string[];
|
|
234
300
|
files?: string[];
|
|
301
|
+
deferred?: boolean;
|
|
235
302
|
};
|
|
236
303
|
}>(
|
|
237
304
|
"/api/tasks",
|
|
@@ -244,6 +311,7 @@ export function createServer(app: App) {
|
|
|
244
311
|
title: { type: "string" },
|
|
245
312
|
sections: { type: "array", items: { type: "string" } },
|
|
246
313
|
files: { type: "array", items: { type: "string" } },
|
|
314
|
+
deferred: { type: "boolean" },
|
|
247
315
|
},
|
|
248
316
|
},
|
|
249
317
|
},
|
|
@@ -251,17 +319,29 @@ export function createServer(app: App) {
|
|
|
251
319
|
(request) => {
|
|
252
320
|
const d = request.body;
|
|
253
321
|
return app.taskInfo(
|
|
254
|
-
app.tasks.create(d.name, d.title, d.sections, d.files).name,
|
|
322
|
+
app.tasks.create(d.name, d.title, d.sections, d.files, d.deferred).name,
|
|
255
323
|
);
|
|
256
324
|
},
|
|
257
325
|
);
|
|
258
326
|
server.post<{ Params: { name: string } }>(
|
|
259
327
|
"/api/tasks/:name/publish",
|
|
260
328
|
(request) => {
|
|
329
|
+
app.delivery.assertReady(request.params.name);
|
|
261
330
|
const job = app.publisher.submit(request.params.name);
|
|
262
331
|
return { job: job.id, status: job.status };
|
|
263
332
|
},
|
|
264
333
|
);
|
|
334
|
+
// The task's typeset pages as images, for its agent to look at.
|
|
335
|
+
server.post<{ Params: { name: string }; Body: ViewRequest }>(
|
|
336
|
+
"/api/tasks/:name/view",
|
|
337
|
+
{ schema: { body: { ...objectSchema, additionalProperties: false, properties: { locations: { type: "array", maxItems: 8, items: { type: "string", maxLength: 1000 } }, pages: { type: "array", maxItems: 8, items: { type: "string", maxLength: 40 } }, accepted: { type: "boolean" } } } } },
|
|
338
|
+
(request) => view(app, request.params.name, request.body),
|
|
339
|
+
);
|
|
340
|
+
server.post<{ Params: { name: string }; Body: { mode: "workspace" | "branch"; message?: string } }>(
|
|
341
|
+
"/api/tasks/:name/deliver",
|
|
342
|
+
{ schema: { body: { ...objectSchema, required: ["mode"], properties: { mode: { enum: ["workspace", "branch"] }, message: { type: "string", maxLength: 5000 } } } } },
|
|
343
|
+
request => app.delivery.deliver(request.params.name, request.body.mode, request.body.message),
|
|
344
|
+
);
|
|
265
345
|
server.post<{
|
|
266
346
|
Params: { name: string };
|
|
267
347
|
Body: {
|
|
@@ -39,6 +39,7 @@ export interface Scope {
|
|
|
39
39
|
sections: SectionRef[];
|
|
40
40
|
}
|
|
41
41
|
export interface Task {
|
|
42
|
+
deferred?: number;
|
|
42
43
|
name: string;
|
|
43
44
|
title: string;
|
|
44
45
|
worktree: string;
|
|
@@ -48,6 +49,17 @@ export interface Task {
|
|
|
48
49
|
created: number;
|
|
49
50
|
closed: number | null;
|
|
50
51
|
}
|
|
52
|
+
export interface Delivery {
|
|
53
|
+
mode: "workspace" | "branch";
|
|
54
|
+
accepted: string;
|
|
55
|
+
seq: number;
|
|
56
|
+
branch?: string;
|
|
57
|
+
commit?: string;
|
|
58
|
+
parent?: string;
|
|
59
|
+
previous?: string;
|
|
60
|
+
done: boolean;
|
|
61
|
+
writes: { file: string; before: string | null; after: string | null }[];
|
|
62
|
+
}
|
|
51
63
|
export interface Unit {
|
|
52
64
|
id: string;
|
|
53
65
|
file: string;
|
|
@@ -227,6 +239,130 @@ export interface RenderedRound {
|
|
|
227
239
|
units: Record<string, VisualUnit>;
|
|
228
240
|
sections: { node: string; number: string; title: string; blocks: Block[] }[];
|
|
229
241
|
loose: ChangeBlock[];
|
|
242
|
+
references?: References;
|
|
243
|
+
}
|
|
244
|
+
/** One field of a changed bibliography entry, before and after; null
|
|
245
|
+
* where the entry lacks it. `resolved` is the new value with @string
|
|
246
|
+
* macros expanded, when that differs; `oldResolved` is its old expansion. */
|
|
247
|
+
export interface ReferenceField {
|
|
248
|
+
name: string;
|
|
249
|
+
old: string | null;
|
|
250
|
+
new: string | null;
|
|
251
|
+
resolved?: string;
|
|
252
|
+
oldResolved?: string;
|
|
253
|
+
}
|
|
254
|
+
export interface ReferenceCitation {
|
|
255
|
+
file: string;
|
|
256
|
+
line: number;
|
|
257
|
+
section: string | null;
|
|
258
|
+
}
|
|
259
|
+
/** still-cited: a removed key that the document still cites.
|
|
260
|
+
* not-in-bibliography: the document does not read this .bib file. */
|
|
261
|
+
export type ReferenceProblem =
|
|
262
|
+
| "still-cited"
|
|
263
|
+
| "duplicate-key"
|
|
264
|
+
| "unreadable"
|
|
265
|
+
| "not-in-bibliography";
|
|
266
|
+
/** A bibliography entry that a round adds, removes or modifies, shown whole
|
|
267
|
+
* under the edit it belongs to. */
|
|
268
|
+
export interface ReferenceEntry {
|
|
269
|
+
id: string;
|
|
270
|
+
unit: string;
|
|
271
|
+
/** All edits affecting this entry when macros span multiple files. */
|
|
272
|
+
units?: string[];
|
|
273
|
+
file: string;
|
|
274
|
+
key: string;
|
|
275
|
+
old_key?: string;
|
|
276
|
+
type: string;
|
|
277
|
+
old_type?: string;
|
|
278
|
+
status: "added" | "removed" | "modified";
|
|
279
|
+
fields: ReferenceField[];
|
|
280
|
+
lines: Span;
|
|
281
|
+
/** Where the revised document cites the key. */
|
|
282
|
+
cited: ReferenceCitation[];
|
|
283
|
+
/** Where it still cites a renamed entry's old key. */
|
|
284
|
+
old_cited?: ReferenceCitation[];
|
|
285
|
+
problems: ReferenceProblem[];
|
|
286
|
+
error?: string;
|
|
287
|
+
}
|
|
288
|
+
/** A key the revised document newly cites with no bibliography entry. */
|
|
289
|
+
export interface MissingCitation {
|
|
290
|
+
key: string;
|
|
291
|
+
file: string;
|
|
292
|
+
line: number;
|
|
293
|
+
unit: string | null;
|
|
294
|
+
section: string | null;
|
|
295
|
+
}
|
|
296
|
+
export interface References {
|
|
297
|
+
entries: ReferenceEntry[];
|
|
298
|
+
missing: MissingCitation[];
|
|
299
|
+
}
|
|
300
|
+
/** A published record a bibliography entry was compared with. */
|
|
301
|
+
export interface CheckRecord {
|
|
302
|
+
source: "doi" | "crossref" | "arxiv" | "dblp";
|
|
303
|
+
title: string;
|
|
304
|
+
authors: string[];
|
|
305
|
+
year: number | null;
|
|
306
|
+
venue: string | null;
|
|
307
|
+
doi: string | null;
|
|
308
|
+
url: string;
|
|
309
|
+
}
|
|
310
|
+
export interface CheckDifference {
|
|
311
|
+
field: "title" | "author" | "year" | "doi" | "arxiv";
|
|
312
|
+
bib: string;
|
|
313
|
+
record: string;
|
|
314
|
+
/** Shown, but not a reason to doubt the entry: punctuation in a title,
|
|
315
|
+
* or a preprint's year before the published version's. */
|
|
316
|
+
minor?: boolean;
|
|
317
|
+
}
|
|
318
|
+
/** verified: a record agrees. mismatch: a record of the same work differs,
|
|
319
|
+
* or the DOI names another work. unverified: no record was found.
|
|
320
|
+
* failed: a source could not be reached. skipped: nothing to look up. */
|
|
321
|
+
export interface ReferenceCheck {
|
|
322
|
+
status: "verified" | "mismatch" | "unverified" | "failed" | "skipped";
|
|
323
|
+
record?: CheckRecord;
|
|
324
|
+
differences: CheckDifference[];
|
|
325
|
+
/** What the entry was looked up by. */
|
|
326
|
+
via: "doi" | "arxiv" | "title" | null;
|
|
327
|
+
reason?: "doi-not-found" | "arxiv-not-found" | "no-match" | "nothing-to-check";
|
|
328
|
+
/** Sources that could not be reached for this check. */
|
|
329
|
+
unavailable: string[];
|
|
330
|
+
checked: number;
|
|
331
|
+
}
|
|
332
|
+
export interface ReferenceChecks {
|
|
333
|
+
checks: Record<string, ReferenceCheck>;
|
|
334
|
+
/** Entries still being looked up. */
|
|
335
|
+
pending: number;
|
|
336
|
+
/** Entries the author has checked by hand, with when (seconds). A
|
|
337
|
+
* confirmation holds while the entry's looked-up fields stay the same. */
|
|
338
|
+
confirmed?: Record<string, number>;
|
|
339
|
+
}
|
|
340
|
+
/** An entry of the paper's bibliography as it is now. */
|
|
341
|
+
export interface BibliographyEntry {
|
|
342
|
+
id: string;
|
|
343
|
+
file: string;
|
|
344
|
+
key: string;
|
|
345
|
+
type: string;
|
|
346
|
+
fields: { name: string; value: string; resolved?: string }[];
|
|
347
|
+
lines: Span;
|
|
348
|
+
cited: ReferenceCitation[];
|
|
349
|
+
error?: string;
|
|
350
|
+
}
|
|
351
|
+
/** The whole bibliography of a paper: its entries, where each is cited,
|
|
352
|
+
* and what is wrong with them without looking anything up. */
|
|
353
|
+
export interface Bibliography {
|
|
354
|
+
/** The .bib files the paper names, and those of them that do not exist. */
|
|
355
|
+
bibs: string[];
|
|
356
|
+
absent: string[];
|
|
357
|
+
entries: BibliographyEntry[];
|
|
358
|
+
/** Citations of keys no entry defines, each place. */
|
|
359
|
+
missing: (ReferenceCitation & { key: string })[];
|
|
360
|
+
/** Entries sharing a key. */
|
|
361
|
+
duplicates: string[][];
|
|
362
|
+
/** Entries under different keys with the same DOI or title. */
|
|
363
|
+
similar: { ids: string[]; by: "doi" | "title" }[];
|
|
364
|
+
/** \nocite{*} puts every entry in the bibliography. */
|
|
365
|
+
all_cited: boolean;
|
|
230
366
|
}
|
|
231
367
|
export interface RenderRequest {
|
|
232
368
|
main: string;
|
|
@@ -261,6 +397,7 @@ export interface RoundDetail extends Omit<RenderedRound, "units"> {
|
|
|
261
397
|
units: (Unit & Partial<VisualUnit> & { history?: History[] })[];
|
|
262
398
|
warnings: string[];
|
|
263
399
|
files: string;
|
|
400
|
+
references?: References;
|
|
264
401
|
}
|
|
265
402
|
export type TaskState =
|
|
266
403
|
"editing" | "building" | "build_failed" | "reviewing" | "revise" | "done";
|
|
@@ -285,6 +422,8 @@ export interface PageState {
|
|
|
285
422
|
* and how many manuscript files this task's accepted changes touched.
|
|
286
423
|
*/
|
|
287
424
|
workspace?: {
|
|
425
|
+
deferred?: boolean;
|
|
426
|
+
destination?: "workspace" | "branch";
|
|
288
427
|
status: string;
|
|
289
428
|
unsent: number;
|
|
290
429
|
comments?: number;
|
|
@@ -292,6 +431,8 @@ export interface PageState {
|
|
|
292
431
|
};
|
|
293
432
|
state: TaskState | null;
|
|
294
433
|
latest: number | null;
|
|
434
|
+
/** Publication times for browsing earlier changes without revision numbers. */
|
|
435
|
+
history?: { number: number; created: number }[];
|
|
295
436
|
round: number | null;
|
|
296
437
|
revision: string | null;
|
|
297
438
|
decisions: Record<string, Decision>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Shared with the host settings; storage events also update standalone tabs.
|
|
2
|
+
export const EFFECTS_STORAGE = "latex-review:effects";
|
|
3
|
+
export const EFFECTS_EVENT = "latex-review:effects-changed";
|
|
4
|
+
export function readExpandedEffects() {
|
|
5
|
+
try {
|
|
6
|
+
return localStorage.getItem(EFFECTS_STORAGE) === "on";
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -28,7 +28,7 @@ export function withLanguage(href: string) {
|
|
|
28
28
|
: `${href}${href.includes("?") ? "&" : "?"}lang=${current}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// t("
|
|
31
|
+
// t("Change {n}", { n }) fills {placeholders} after translating.
|
|
32
32
|
export function t(text: string, values?: Record<string, string | number>) {
|
|
33
33
|
const template = messages[current][text] ?? text;
|
|
34
34
|
return values
|
|
@@ -100,11 +100,7 @@ export function Overview() {
|
|
|
100
100
|
</div>
|
|
101
101
|
<div className="task-meta">
|
|
102
102
|
<span className="task-name">{task.task}</span>
|
|
103
|
-
<span>
|
|
104
|
-
{task.round
|
|
105
|
-
? t("Round {round}", { round: task.round })
|
|
106
|
-
: t("Not published yet")}
|
|
107
|
-
</span>
|
|
103
|
+
{!task.round && <span>{t("Not published yet")}</span>}
|
|
108
104
|
</div>
|
|
109
105
|
{total > 0 && (
|
|
110
106
|
<div className="task-progress">
|