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
|
@@ -18,8 +18,17 @@ import type {
|
|
|
18
18
|
} from "../types.ts";
|
|
19
19
|
import { PdfStrip } from "./PdfStrip.tsx";
|
|
20
20
|
import { ChangeMap } from "./ChangeMap.tsx";
|
|
21
|
+
import { EFFECTS_EVENT, EFFECTS_STORAGE, readExpandedEffects } from "./effect-preferences.ts";
|
|
21
22
|
import { STATUS } from "./overview.tsx";
|
|
22
|
-
import { t, tn, withLanguage } from "./i18n.ts";
|
|
23
|
+
import { language, locale, t, tn, withLanguage } from "./i18n.ts";
|
|
24
|
+
import {
|
|
25
|
+
ConfirmReferences,
|
|
26
|
+
ReferenceBanner,
|
|
27
|
+
ReferenceEntries,
|
|
28
|
+
risks,
|
|
29
|
+
useReferenceChecks,
|
|
30
|
+
} from "./references.tsx";
|
|
31
|
+
import { bindingLabel, readShortcuts, resolveShortcuts, shortcutAction, SHORTCUT_EVENT, SHORTCUT_STORAGE } from "./shortcuts.ts";
|
|
23
32
|
import {
|
|
24
33
|
STATE,
|
|
25
34
|
buildStops,
|
|
@@ -91,12 +100,29 @@ function jobText(job: PageState["job"]) {
|
|
|
91
100
|
? job.ahead
|
|
92
101
|
? tn(
|
|
93
102
|
job.ahead,
|
|
94
|
-
"Queued behind {n}
|
|
95
|
-
"Queued behind {n}
|
|
103
|
+
"Queued behind {n} update…",
|
|
104
|
+
"Queued behind {n} updates…",
|
|
96
105
|
)
|
|
97
106
|
: t("Queued…")
|
|
98
107
|
: `${t(PHASE[job?.phase ?? ""] || "Starting")}…`;
|
|
99
108
|
}
|
|
109
|
+
|
|
110
|
+
function ReviewProgress({ value, total, label }: { value: number; total: number; label: string }) {
|
|
111
|
+
if (total === 0) return null;
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className="meter review-progress"
|
|
115
|
+
role="progressbar"
|
|
116
|
+
aria-label={label}
|
|
117
|
+
aria-valuemin={0}
|
|
118
|
+
aria-valuemax={total}
|
|
119
|
+
aria-valuenow={value}
|
|
120
|
+
title={label}
|
|
121
|
+
>
|
|
122
|
+
<span aria-hidden="true"><span className="review-progress-value">{value}</span> / {total}</span>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
100
126
|
export function ReviewApp({
|
|
101
127
|
task,
|
|
102
128
|
pinned,
|
|
@@ -119,15 +145,50 @@ export function ReviewApp({
|
|
|
119
145
|
Record<string, PlaceView[] | "loading" | { error: string }>
|
|
120
146
|
>({}),
|
|
121
147
|
[composer, setComposer] = useState<Composer | null>(null),
|
|
148
|
+
// Accepting bibliography entries that are not verified waits for this.
|
|
149
|
+
[confirm, setConfirm] = useState<{
|
|
150
|
+
kind: Kind;
|
|
151
|
+
chosen: string[];
|
|
152
|
+
body: string;
|
|
153
|
+
items: { label: string; reasons: string[] }[];
|
|
154
|
+
} | null>(null),
|
|
122
155
|
[draft, setDraft] = useState(""),
|
|
123
156
|
[help, setHelp] = useState(false),
|
|
157
|
+
// A note can reveal a decided change without opening the history controls.
|
|
158
|
+
[linkedChange] = useState(() => {
|
|
159
|
+
const value = Number(new URLSearchParams(location.search).get("change") ?? -1);
|
|
160
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : -1;
|
|
161
|
+
}),
|
|
124
162
|
[history, setHistory] = useState(
|
|
125
|
-
Boolean(pinned) || new URLSearchParams(location.search).has("history"),
|
|
163
|
+
(Boolean(pinned) && linkedChange < 0) || new URLSearchParams(location.search).has("history"),
|
|
126
164
|
);
|
|
127
165
|
const paper = useRef<HTMLElement>(null),
|
|
128
166
|
epoch = useRef(0),
|
|
129
167
|
scrollUntil = useRef(0),
|
|
130
168
|
snapshot = useRef({ state, detail, busy, composer, current });
|
|
169
|
+
const [shortcuts, setShortcuts] = useState(readShortcuts);
|
|
170
|
+
const [expandedEffects, setExpandedEffects] = useState(readExpandedEffects);
|
|
171
|
+
// Per-edit disclosure is temporary and scoped to its round. Changing the
|
|
172
|
+
// global preference resets those overrides without reloading the review.
|
|
173
|
+
const [effectOverrides, setEffectOverrides] = useState<Record<string, boolean>>({});
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const changed = (event: Event) => setExpandedEffects((event as CustomEvent).detail === true);
|
|
176
|
+
const stored = (event: StorageEvent) => { if (event.key === EFFECTS_STORAGE || event.key === null) setExpandedEffects(readExpandedEffects()); };
|
|
177
|
+
window.addEventListener(EFFECTS_EVENT, changed);
|
|
178
|
+
window.addEventListener('storage', stored);
|
|
179
|
+
return () => { window.removeEventListener(EFFECTS_EVENT, changed); window.removeEventListener('storage', stored); };
|
|
180
|
+
}, []);
|
|
181
|
+
useEffect(() => { setEffectOverrides({}); }, [expandedEffects]);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const changed = (event: Event) => setShortcuts(resolveShortcuts((event as CustomEvent).detail));
|
|
184
|
+
const stored = (event: StorageEvent) => { if (event.key === SHORTCUT_STORAGE || event.key === null) setShortcuts(readShortcuts()); };
|
|
185
|
+
window.addEventListener(SHORTCUT_EVENT, changed);
|
|
186
|
+
window.addEventListener('storage', stored);
|
|
187
|
+
return () => { window.removeEventListener(SHORTCUT_EVENT, changed); window.removeEventListener('storage', stored); };
|
|
188
|
+
}, []);
|
|
189
|
+
function showHelp() {
|
|
190
|
+
if (window.dispatchEvent(new Event('latex-review:help', { cancelable: true }))) setHelp(true);
|
|
191
|
+
}
|
|
131
192
|
snapshot.current = { state, detail, busy, composer, current };
|
|
132
193
|
const units = useMemo(
|
|
133
194
|
() => new Map(detail?.units.map((u) => [u.id, u]) ?? []),
|
|
@@ -157,6 +218,13 @@ export function ReviewApp({
|
|
|
157
218
|
undecided = Object.keys(decisions).filter(
|
|
158
219
|
(id) => decisions[id] === "pending",
|
|
159
220
|
),
|
|
221
|
+
references = useReferenceChecks(detail, api),
|
|
222
|
+
hazards = useMemo(
|
|
223
|
+
() => risks(detail?.references, references.checks, references.pending, references.confirmed),
|
|
224
|
+
[detail, references.checks, references.pending, references.confirmed],
|
|
225
|
+
),
|
|
226
|
+
// Batch acceptance leaves the changes whose entries need a look.
|
|
227
|
+
batch = undecided.filter((id) => !hazards.has(id)),
|
|
160
228
|
latest = Boolean(
|
|
161
229
|
detail && state && detail.round === state.latest && !state.closed,
|
|
162
230
|
),
|
|
@@ -164,6 +232,9 @@ export function ReviewApp({
|
|
|
164
232
|
label = (kind: string) => t((changes ? CHANGE_ACTION : ACTION)[kind] ?? kind),
|
|
165
233
|
stateLabel = (value: string) =>
|
|
166
234
|
t((changes ? CHANGE_STATE : STATE)[value] ?? value);
|
|
235
|
+
// Keyboard handlers read the newest checks, not those of their render.
|
|
236
|
+
const hazardsNow = useRef(hazards);
|
|
237
|
+
hazardsNow.current = hazards;
|
|
167
238
|
// What each button acts on: undecided changes, or in changes mode the
|
|
168
239
|
// staged ones for Unstage and every change still on disk for Revert.
|
|
169
240
|
const targets = (units: string[], kind: Kind) =>
|
|
@@ -184,7 +255,7 @@ export function ReviewApp({
|
|
|
184
255
|
: [];
|
|
185
256
|
const navigation = stops
|
|
186
257
|
.map((stop, index) => ({ stop, index }))
|
|
187
|
-
.filter(({ stop }) => browsing || pendingUnits(stop, decisions).length)
|
|
258
|
+
.filter(({ stop, index }) => browsing || index === linkedChange || pendingUnits(stop, decisions).length)
|
|
188
259
|
.map(({ index }) => index);
|
|
189
260
|
const adjacent = (index: number, direction: number) =>
|
|
190
261
|
navigation[navigation.indexOf(index) + direction] ?? -1;
|
|
@@ -247,11 +318,20 @@ export function ReviewApp({
|
|
|
247
318
|
const article = paper.current?.querySelector<HTMLElement>(
|
|
248
319
|
`[data-stop="${index}"]`,
|
|
249
320
|
);
|
|
250
|
-
article
|
|
321
|
+
if (article) reveal(article, "smooth");
|
|
251
322
|
article?.focus({ preventScroll: true });
|
|
252
323
|
});
|
|
253
324
|
}
|
|
254
325
|
}
|
|
326
|
+
function reveal(article: HTMLElement, behavior: ScrollBehavior = "instant") {
|
|
327
|
+
const root = paper.current;
|
|
328
|
+
if (!root) return;
|
|
329
|
+
// scrollIntoView also scrolls ancestors across the embedding iframe.
|
|
330
|
+
// Navigation belongs to the paper, independently of the host's Chat.
|
|
331
|
+
const padding = parseFloat(getComputedStyle(root).scrollPaddingTop) || 0;
|
|
332
|
+
const margin = parseFloat(getComputedStyle(article).scrollMarginTop) || 0;
|
|
333
|
+
root.scrollTo({ top: root.scrollTop + article.getBoundingClientRect().top - root.getBoundingClientRect().top - padding - margin, behavior });
|
|
334
|
+
}
|
|
255
335
|
useEffect(() => {
|
|
256
336
|
let cancelled = false,
|
|
257
337
|
timer: ReturnType<typeof setTimeout>;
|
|
@@ -302,6 +382,7 @@ export function ReviewApp({
|
|
|
302
382
|
!pendingUnits(nextStops[index], loadedState.decisions).length
|
|
303
383
|
)
|
|
304
384
|
index = nextPending(nextStops, loadedState.decisions, index);
|
|
385
|
+
if (linkedChange >= 0 && nextStops[linkedChange]) index = linkedChange;
|
|
305
386
|
if (index < 0) index = pinned && nextStops.length ? 0 : -1;
|
|
306
387
|
if (
|
|
307
388
|
previous.detail &&
|
|
@@ -314,7 +395,7 @@ export function ReviewApp({
|
|
|
314
395
|
setCurrent(index);
|
|
315
396
|
setOpenSource(new Set());
|
|
316
397
|
if (previous.detail)
|
|
317
|
-
setToast(t("
|
|
398
|
+
setToast(t("Changes are ready"));
|
|
318
399
|
} else setState(next);
|
|
319
400
|
} catch (e) {
|
|
320
401
|
if (!cancelled) {
|
|
@@ -334,11 +415,10 @@ export function ReviewApp({
|
|
|
334
415
|
}, [task, pinned]);
|
|
335
416
|
useEffect(() => {
|
|
336
417
|
if (detail && current >= 0)
|
|
337
|
-
requestAnimationFrame(() =>
|
|
338
|
-
paper.current
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
);
|
|
418
|
+
requestAnimationFrame(() => {
|
|
419
|
+
const article = paper.current?.querySelector<HTMLElement>(`[data-stop="${current}"]`);
|
|
420
|
+
if (article) reveal(article);
|
|
421
|
+
});
|
|
342
422
|
}, [detail?.round]);
|
|
343
423
|
useEffect(() => {
|
|
344
424
|
if (state) document.title = `${t(state.title)} · LaTeX Review`;
|
|
@@ -373,6 +453,7 @@ export function ReviewApp({
|
|
|
373
453
|
kind: Kind,
|
|
374
454
|
chosen = targets(stops[current]?.units ?? [], kind),
|
|
375
455
|
body = "",
|
|
456
|
+
confirmed = false,
|
|
376
457
|
) {
|
|
377
458
|
if (
|
|
378
459
|
!chosen.length ||
|
|
@@ -382,6 +463,13 @@ export function ReviewApp({
|
|
|
382
463
|
blocked(kind)
|
|
383
464
|
)
|
|
384
465
|
return;
|
|
466
|
+
if (kind === "accept" && !confirmed) {
|
|
467
|
+
const items = chosen.flatMap((id) => hazardsNow.current.get(id) ?? []);
|
|
468
|
+
if (items.length) {
|
|
469
|
+
setConfirm({ kind, chosen, body, items });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
385
473
|
epoch.current++;
|
|
386
474
|
setBusy(true);
|
|
387
475
|
snapshot.current.busy = true;
|
|
@@ -490,11 +578,13 @@ export function ReviewApp({
|
|
|
490
578
|
}
|
|
491
579
|
useEffect(() => {
|
|
492
580
|
const listener = (event: KeyboardEvent) => {
|
|
581
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
493
582
|
const target = event.target as HTMLElement,
|
|
494
583
|
typing =
|
|
495
584
|
target.matches("input,textarea,select") || target.isContentEditable;
|
|
496
585
|
if (event.key === "Escape") {
|
|
497
|
-
if (
|
|
586
|
+
if (confirm) setConfirm(null);
|
|
587
|
+
else if (help) setHelp(false);
|
|
498
588
|
else if (composer) closeComposer();
|
|
499
589
|
return;
|
|
500
590
|
}
|
|
@@ -507,53 +597,32 @@ export function ReviewApp({
|
|
|
507
597
|
void submitComposer();
|
|
508
598
|
return;
|
|
509
599
|
}
|
|
510
|
-
if (typing || help) return;
|
|
511
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "z") {
|
|
600
|
+
if (typing || help || confirm || window.getSelection()?.toString()) return;
|
|
601
|
+
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "z") {
|
|
512
602
|
event.preventDefault();
|
|
513
603
|
void undo();
|
|
514
604
|
return;
|
|
515
605
|
}
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
window.getSelection()?.toString()
|
|
522
|
-
)
|
|
523
|
-
return;
|
|
524
|
-
const key = event.key.toLowerCase();
|
|
525
|
-
if (
|
|
526
|
-
[
|
|
527
|
-
"a",
|
|
528
|
-
"r",
|
|
529
|
-
"c",
|
|
530
|
-
"j",
|
|
531
|
-
"k",
|
|
532
|
-
"arrowright",
|
|
533
|
-
"arrowleft",
|
|
534
|
-
"n",
|
|
535
|
-
"s",
|
|
536
|
-
"u",
|
|
537
|
-
"?",
|
|
538
|
-
].includes(key)
|
|
539
|
-
)
|
|
540
|
-
event.preventDefault();
|
|
541
|
-
if (key === "a")
|
|
606
|
+
if (composer) return;
|
|
607
|
+
const action = shortcutAction(event, shortcuts);
|
|
608
|
+
if (!action || (changes && action === 'comment')) return;
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
if (action === 'accept')
|
|
542
611
|
void decide(kinds(stops[current]?.units ?? [])[0] ?? "accept");
|
|
543
|
-
else if (
|
|
544
|
-
else if (
|
|
545
|
-
else if (
|
|
612
|
+
else if (action === 'reject') void decide("reject");
|
|
613
|
+
else if (action === 'comment') beginComposer();
|
|
614
|
+
else if (action === 'next')
|
|
546
615
|
select(adjacent(current, 1));
|
|
547
|
-
else if (
|
|
616
|
+
else if (action === 'previous')
|
|
548
617
|
select(adjacent(current, -1));
|
|
549
|
-
else if (
|
|
550
|
-
else if (
|
|
551
|
-
else if (
|
|
552
|
-
else if (
|
|
618
|
+
else if (action === 'pending') select(nextPending(stops, decisions, current));
|
|
619
|
+
else if (action === 'source') toggleSource(current);
|
|
620
|
+
else if (action === 'undo') void undo();
|
|
621
|
+
else if (action === 'help') showHelp();
|
|
553
622
|
};
|
|
554
623
|
window.addEventListener("keydown", listener);
|
|
555
624
|
return () => window.removeEventListener("keydown", listener);
|
|
556
|
-
}, [state, detail, current, busy, composer, draft, help, history]);
|
|
625
|
+
}, [state, detail, current, busy, composer, draft, help, confirm, history, shortcuts]);
|
|
557
626
|
function onScroll() {
|
|
558
627
|
if (
|
|
559
628
|
!paper.current ||
|
|
@@ -589,8 +658,8 @@ export function ReviewApp({
|
|
|
589
658
|
<button
|
|
590
659
|
className="nav"
|
|
591
660
|
aria-label={t("Previous change")}
|
|
592
|
-
aria-keyshortcuts=
|
|
593
|
-
title={t(
|
|
661
|
+
aria-keyshortcuts={shortcuts.previous.join(' ')}
|
|
662
|
+
title={[t('Previous change'), shortcuts.previous.map(bindingLabel).join(' / ')].filter(Boolean).join(' · ')}
|
|
594
663
|
disabled={adjacent(index, -1) < 0}
|
|
595
664
|
onClick={() => select(adjacent(index, -1))}
|
|
596
665
|
>
|
|
@@ -604,16 +673,16 @@ export function ReviewApp({
|
|
|
604
673
|
{available.length ? (
|
|
605
674
|
<>
|
|
606
675
|
{available.map((kind) => {
|
|
607
|
-
const
|
|
676
|
+
const keys = shortcuts[kind === 'unstage' ? 'accept' : kind],
|
|
608
677
|
count = targets(units, kind).length;
|
|
609
678
|
return (
|
|
610
679
|
<button
|
|
611
680
|
className={kind}
|
|
612
681
|
key={kind}
|
|
613
|
-
aria-keyshortcuts={
|
|
682
|
+
aria-keyshortcuts={keys.join(' ')}
|
|
614
683
|
title={
|
|
615
684
|
actionLock(kind) ||
|
|
616
|
-
|
|
685
|
+
[label(kind), keys.map(bindingLabel).join(' / ')].filter(Boolean).join(' · ')
|
|
617
686
|
}
|
|
618
687
|
aria-describedby={
|
|
619
688
|
actionLock(kind) ? "review-lock" : undefined
|
|
@@ -632,7 +701,7 @@ export function ReviewApp({
|
|
|
632
701
|
) : remaining >= 0 ? (
|
|
633
702
|
<button
|
|
634
703
|
className="skip"
|
|
635
|
-
aria-keyshortcuts=
|
|
704
|
+
aria-keyshortcuts={shortcuts.pending.join(' ')}
|
|
636
705
|
onClick={() => select(nextPending(stops, decisions, current))}
|
|
637
706
|
>
|
|
638
707
|
{changes ? t("Next unstaged") : t("Next undecided")}
|
|
@@ -643,15 +712,15 @@ export function ReviewApp({
|
|
|
643
712
|
? stateLabel(stopState(units, decisions))
|
|
644
713
|
: latest
|
|
645
714
|
? t("Review complete")
|
|
646
|
-
: t("Earlier
|
|
715
|
+
: t("Earlier changes")}
|
|
647
716
|
</span>
|
|
648
717
|
)}
|
|
649
718
|
{navigation.length > 1 && (
|
|
650
719
|
<button
|
|
651
720
|
className="nav"
|
|
652
721
|
aria-label={t("Next change")}
|
|
653
|
-
aria-keyshortcuts=
|
|
654
|
-
title={t(
|
|
722
|
+
aria-keyshortcuts={shortcuts.next.join(' ')}
|
|
723
|
+
title={[t('Next change'), shortcuts.next.map(bindingLabel).join(' / ')].filter(Boolean).join(' · ')}
|
|
655
724
|
disabled={adjacent(index, 1) < 0}
|
|
656
725
|
onClick={() => select(adjacent(index, 1))}
|
|
657
726
|
>
|
|
@@ -683,7 +752,10 @@ export function ReviewApp({
|
|
|
683
752
|
clean: boolean,
|
|
684
753
|
): ReactNode {
|
|
685
754
|
const { total, shown } = unit.effects!,
|
|
686
|
-
|
|
755
|
+
key = `${detail!.round}:${unit.id}`,
|
|
756
|
+
open = effectOverrides[key] ?? (expandedEffects || unit.visual === "unmapped" || unit.visual === "identical"),
|
|
757
|
+
bodyId = `effects-${detail!.round}-${unit.id}`,
|
|
758
|
+
loaded = more[key],
|
|
687
759
|
places = [...shown, ...(Array.isArray(loaded) ? loaded : [])],
|
|
688
760
|
strips = (items: Strip[], side: "before" | "after") =>
|
|
689
761
|
items.length > 0 && (
|
|
@@ -705,47 +777,57 @@ export function ReviewApp({
|
|
|
705
777
|
return (
|
|
706
778
|
<section className="effects" key={unit.id}>
|
|
707
779
|
<p className="effects-head">
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<p className="place-where">
|
|
713
|
-
{t("p. {pages}", { pages: place.page })}
|
|
714
|
-
{place.section ? ` · ${place.section}` : ""}
|
|
715
|
-
{place.with
|
|
716
|
-
? ` · ${t("with {changes}", {
|
|
717
|
-
changes: place.with
|
|
718
|
-
.map((id) =>
|
|
719
|
-
firstStop.has(id)
|
|
720
|
-
? t("Change {n}", { n: firstStop.get(id)! + 1 })
|
|
721
|
-
: (units.get(id)?.file ?? id),
|
|
722
|
-
)
|
|
723
|
-
.join(", "),
|
|
724
|
-
})}`
|
|
725
|
-
: ""}
|
|
726
|
-
</p>
|
|
727
|
-
{strips(place.before, "before")}
|
|
728
|
-
{strips(place.after, "after")}
|
|
729
|
-
</div>
|
|
730
|
-
))}
|
|
731
|
-
{total > places.length && (
|
|
732
|
-
<button
|
|
733
|
-
className="link more-places"
|
|
734
|
-
disabled={loaded === "loading"}
|
|
735
|
-
onClick={() => void loadPlaces(unit.id)}
|
|
736
|
-
>
|
|
737
|
-
{loaded === "loading"
|
|
738
|
-
? t("Loading…")
|
|
739
|
-
: tn(
|
|
740
|
-
total - places.length,
|
|
741
|
-
"Show {n} more",
|
|
742
|
-
"Show {n} more",
|
|
743
|
-
)}
|
|
780
|
+
<button className="link effects-toggle" aria-expanded={open} aria-controls={bodyId}
|
|
781
|
+
onClick={() => setEffectOverrides((values) => ({ ...values, [key]: !open }))}>
|
|
782
|
+
<span aria-hidden="true">{open ? "▾" : "▸"}</span>{" "}
|
|
783
|
+
{tn(total, "Affects {n} place", "Affects {n} places")}
|
|
744
784
|
</button>
|
|
745
|
-
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
785
|
+
</p>
|
|
786
|
+
<div id={bodyId} hidden={!open}>
|
|
787
|
+
{open && (
|
|
788
|
+
<>
|
|
789
|
+
{places.map((place, i) => (
|
|
790
|
+
<div className="place" key={i}>
|
|
791
|
+
<p className="place-where">
|
|
792
|
+
{t("p. {pages}", { pages: place.page })}
|
|
793
|
+
{place.section ? ` · ${place.section}` : ""}
|
|
794
|
+
{place.with
|
|
795
|
+
? ` · ${t("with {changes}", {
|
|
796
|
+
changes: place.with
|
|
797
|
+
.map((id) =>
|
|
798
|
+
firstStop.has(id)
|
|
799
|
+
? t("Change {n}", { n: firstStop.get(id)! + 1 })
|
|
800
|
+
: (units.get(id)?.file ?? id),
|
|
801
|
+
)
|
|
802
|
+
.join(", "),
|
|
803
|
+
})}`
|
|
804
|
+
: ""}
|
|
805
|
+
</p>
|
|
806
|
+
{strips(place.before, "before")}
|
|
807
|
+
{strips(place.after, "after")}
|
|
808
|
+
</div>
|
|
809
|
+
))}
|
|
810
|
+
{total > places.length && (
|
|
811
|
+
<button
|
|
812
|
+
className="link more-places"
|
|
813
|
+
disabled={loaded === "loading"}
|
|
814
|
+
onClick={() => void loadPlaces(unit.id)}
|
|
815
|
+
>
|
|
816
|
+
{loaded === "loading"
|
|
817
|
+
? t("Loading…")
|
|
818
|
+
: tn(
|
|
819
|
+
total - places.length,
|
|
820
|
+
"Show {n} more",
|
|
821
|
+
"Show {n} more",
|
|
822
|
+
)}
|
|
823
|
+
</button>
|
|
824
|
+
)}
|
|
825
|
+
{loaded && typeof loaded === "object" && "error" in loaded && (
|
|
826
|
+
<p className="caption">{loaded.error}</p>
|
|
827
|
+
)}
|
|
828
|
+
</>
|
|
829
|
+
)}
|
|
830
|
+
</div>
|
|
749
831
|
</section>
|
|
750
832
|
);
|
|
751
833
|
}
|
|
@@ -755,11 +837,16 @@ export function ReviewApp({
|
|
|
755
837
|
inline: boolean,
|
|
756
838
|
): ReactNode {
|
|
757
839
|
const known = block.units.map((id) => units.get(id)!).filter(Boolean),
|
|
840
|
+
// A .bib edit is shown entry by entry, every field, rather than as
|
|
841
|
+
// the bibliography's typeset lines alone.
|
|
842
|
+
bib = detail!.references?.entries.filter((e) => (e.units ?? [e.unit]).some((id) => block.units.includes(id))) ?? [],
|
|
843
|
+
uncited = detail!.references?.missing.filter((m) => m.unit && block.units.includes(m.unit)) ?? [],
|
|
758
844
|
status = stopState(block.units, decisions),
|
|
759
845
|
reviewed = !pendingUnits(stops[index], decisions).length,
|
|
846
|
+
expanded = browsing || index === linkedChange,
|
|
760
847
|
// A reverted change shows the text that is back on disk; staged and
|
|
761
848
|
// unstaged ones keep their before and after.
|
|
762
|
-
resolved = history
|
|
849
|
+
resolved = history || index === linkedChange
|
|
763
850
|
? undefined
|
|
764
851
|
: changes
|
|
765
852
|
? status === "rejected"
|
|
@@ -769,15 +856,17 @@ export function ReviewApp({
|
|
|
769
856
|
(status === "commented" ? "after" : undefined),
|
|
770
857
|
source =
|
|
771
858
|
openSource.has(index) ||
|
|
772
|
-
|
|
773
|
-
|
|
859
|
+
(!bib.length &&
|
|
860
|
+
(block.identical || (!block.before.length && !block.after.length))),
|
|
774
861
|
remarks = [
|
|
775
862
|
...new Set(
|
|
776
863
|
block.units.map((id) => state?.comments[id]).filter(Boolean),
|
|
777
864
|
),
|
|
778
865
|
],
|
|
779
866
|
location = describeLocation(known, t),
|
|
780
|
-
kindLabel =
|
|
867
|
+
kindLabel = bib.length
|
|
868
|
+
? t("Bibliography")
|
|
869
|
+
: block.reflow
|
|
781
870
|
? t("Reflow")
|
|
782
871
|
: [
|
|
783
872
|
...new Set(
|
|
@@ -808,17 +897,17 @@ export function ReviewApp({
|
|
|
808
897
|
return (
|
|
809
898
|
<article
|
|
810
899
|
key={index}
|
|
811
|
-
className={`change ${inline ? "inline" : "standalone"}${current === index ? " current" : ""}${reviewed && !
|
|
900
|
+
className={`change ${inline ? "inline" : "standalone"}${current === index ? " current" : ""}${reviewed && !expanded ? " reviewed-change" : ""}${reviewed && !expanded && !resolved ? " mixed-reviewed" : ""}`}
|
|
812
901
|
data-stop={index}
|
|
813
902
|
data-state={status}
|
|
814
903
|
data-resolved={resolved}
|
|
815
904
|
tabIndex={-1}
|
|
816
905
|
onClick={() => {
|
|
817
|
-
if (index !== current && !composer && (
|
|
906
|
+
if (index !== current && !composer && (expanded || !reviewed))
|
|
818
907
|
select(index, false);
|
|
819
908
|
}}
|
|
820
909
|
>
|
|
821
|
-
{reviewed && !
|
|
910
|
+
{reviewed && !expanded && !resolved && (
|
|
822
911
|
<p className="reviewed-placeholder">
|
|
823
912
|
<button
|
|
824
913
|
className="link"
|
|
@@ -861,7 +950,8 @@ export function ReviewApp({
|
|
|
861
950
|
</button>
|
|
862
951
|
<button
|
|
863
952
|
className={`link ${source ? "active" : ""}`}
|
|
864
|
-
title={t(
|
|
953
|
+
title={[t('Show LaTeX source'), shortcuts.source.map(bindingLabel).join(' / ')].filter(Boolean).join(' · ')}
|
|
954
|
+
aria-keyshortcuts={shortcuts.source.join(' ')}
|
|
865
955
|
onClick={() => toggleSource(index)}
|
|
866
956
|
>
|
|
867
957
|
{block.units.length > 1
|
|
@@ -876,9 +966,7 @@ export function ReviewApp({
|
|
|
876
966
|
.map((h, i) => (
|
|
877
967
|
<p key={i}>
|
|
878
968
|
<strong>
|
|
879
|
-
{t("Revised after your comment
|
|
880
|
-
round: h.round,
|
|
881
|
-
})}
|
|
969
|
+
{t("Revised after your comment: ")}
|
|
882
970
|
</strong>
|
|
883
971
|
“{h.comment}”
|
|
884
972
|
</p>
|
|
@@ -890,7 +978,7 @@ export function ReviewApp({
|
|
|
890
978
|
block.before.length > 0 &&
|
|
891
979
|
images(block.before, "before")}
|
|
892
980
|
{block.after.length > 0 && images(block.after, "after")}
|
|
893
|
-
{block.identical && (
|
|
981
|
+
{block.identical && !bib.length && (
|
|
894
982
|
<p className="caption">
|
|
895
983
|
{t("No visible change in the PDF; see the LaTeX source.")}
|
|
896
984
|
</p>
|
|
@@ -910,6 +998,25 @@ export function ReviewApp({
|
|
|
910
998
|
))}
|
|
911
999
|
</div>
|
|
912
1000
|
)}
|
|
1001
|
+
{uncited.length > 0 && (
|
|
1002
|
+
<p className="reference-problem reference-uncited">
|
|
1003
|
+
{uncited
|
|
1004
|
+
.map((m) => t("Cites {key}, which no bibliography entry defines.", { key: m.key }))
|
|
1005
|
+
.join(language() === "zh-CN" ? "" : " ")}
|
|
1006
|
+
</p>
|
|
1007
|
+
)}
|
|
1008
|
+
{bib.length > 0 && (
|
|
1009
|
+
<ReferenceEntries
|
|
1010
|
+
entries={bib}
|
|
1011
|
+
checks={references.checks}
|
|
1012
|
+
pending={references.pending > 0}
|
|
1013
|
+
onRetry={references.retry}
|
|
1014
|
+
confirmed={references.confirmed}
|
|
1015
|
+
onConfirm={(id, on) =>
|
|
1016
|
+
void references.confirm(id, on).catch((e) => setToast((e as Error).message))
|
|
1017
|
+
}
|
|
1018
|
+
/>
|
|
1019
|
+
)}
|
|
913
1020
|
{source && (
|
|
914
1021
|
<div className="source">
|
|
915
1022
|
{known.map((unit) => (
|
|
@@ -1000,31 +1107,28 @@ export function ReviewApp({
|
|
|
1000
1107
|
{changes
|
|
1001
1108
|
? t("Since the last commit")
|
|
1002
1109
|
: detail
|
|
1003
|
-
? t("
|
|
1004
|
-
(detail.round !== state?.latest ? t(" (earlier round)") : "")
|
|
1110
|
+
? (detail.round === state?.latest ? t("Current changes") : t("Earlier changes"))
|
|
1005
1111
|
: ""}
|
|
1006
1112
|
</span>
|
|
1007
1113
|
</div>
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
</div>
|
|
1022
|
-
)}
|
|
1114
|
+
<ReviewProgress
|
|
1115
|
+
value={changes ? count.accepted : count.decided}
|
|
1116
|
+
total={changes ? count.accepted + count.pending : count.total}
|
|
1117
|
+
label={changes
|
|
1118
|
+
? t("{staged} of {total} staged", {
|
|
1119
|
+
staged: count.accepted,
|
|
1120
|
+
total: count.accepted + count.pending,
|
|
1121
|
+
})
|
|
1122
|
+
: t("{decided} of {total} decided", {
|
|
1123
|
+
decided: count.decided,
|
|
1124
|
+
total: count.total,
|
|
1125
|
+
})}
|
|
1126
|
+
/>
|
|
1023
1127
|
<div className="bar-end">
|
|
1024
1128
|
{/* Batch decisions cover only undecided changes; accepted,
|
|
1025
1129
|
rejected and commented changes keep their state. A single
|
|
1026
1130
|
undecided change is decided from its own bar. */}
|
|
1027
|
-
{count.pending > 1 && (
|
|
1131
|
+
{count.pending > 1 && batch.length > 0 && (
|
|
1028
1132
|
<button
|
|
1029
1133
|
className="ghost accept accept-all"
|
|
1030
1134
|
disabled={
|
|
@@ -1035,30 +1139,41 @@ export function ReviewApp({
|
|
|
1035
1139
|
}
|
|
1036
1140
|
title={
|
|
1037
1141
|
state?.decisionLock ||
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1142
|
+
[
|
|
1143
|
+
changes
|
|
1144
|
+
? tn(
|
|
1145
|
+
batch.length,
|
|
1146
|
+
"Stage the {n} unstaged change.",
|
|
1147
|
+
"Stage the {n} unstaged changes.",
|
|
1148
|
+
)
|
|
1149
|
+
: tn(
|
|
1150
|
+
batch.length,
|
|
1151
|
+
"Accept {n} undecided change",
|
|
1152
|
+
"Accept {n} undecided changes",
|
|
1153
|
+
),
|
|
1154
|
+
undecided.length > batch.length
|
|
1155
|
+
? tn(
|
|
1156
|
+
undecided.length - batch.length,
|
|
1157
|
+
"Leaves {n} change with bibliography entries to check.",
|
|
1158
|
+
"Leaves {n} changes with bibliography entries to check.",
|
|
1159
|
+
)
|
|
1160
|
+
: "",
|
|
1161
|
+
]
|
|
1162
|
+
.filter(Boolean)
|
|
1163
|
+
.join(" ")
|
|
1049
1164
|
}
|
|
1050
1165
|
aria-label={
|
|
1051
1166
|
changes
|
|
1052
|
-
? t("Stage all ({n})", { n:
|
|
1053
|
-
: t("Accept all ({n})", { n:
|
|
1167
|
+
? t("Stage all ({n})", { n: batch.length })
|
|
1168
|
+
: t("Accept all ({n})", { n: batch.length })
|
|
1054
1169
|
}
|
|
1055
1170
|
aria-describedby={state?.decisionLock ? "review-lock" : undefined}
|
|
1056
|
-
onClick={() => void decide("accept",
|
|
1171
|
+
onClick={() => void decide("accept", batch)}
|
|
1057
1172
|
>
|
|
1058
1173
|
<span className="batch-label">
|
|
1059
1174
|
{changes ? t("Stage all") : t("Accept all")}
|
|
1060
1175
|
</span>
|
|
1061
|
-
<span className="batch-count">{
|
|
1176
|
+
<span className="batch-count">{batch.length}</span>
|
|
1062
1177
|
</button>
|
|
1063
1178
|
)}
|
|
1064
1179
|
{count.pending > 1 && !changes && (
|
|
@@ -1074,8 +1189,8 @@ export function ReviewApp({
|
|
|
1074
1189
|
state?.decisionLock ||
|
|
1075
1190
|
tn(
|
|
1076
1191
|
count.pending,
|
|
1077
|
-
"Reject
|
|
1078
|
-
"Reject
|
|
1192
|
+
"Reject {n} undecided change",
|
|
1193
|
+
"Reject {n} undecided changes",
|
|
1079
1194
|
)
|
|
1080
1195
|
}
|
|
1081
1196
|
aria-label={t("Reject all ({n})", { n: count.pending })}
|
|
@@ -1111,7 +1226,8 @@ export function ReviewApp({
|
|
|
1111
1226
|
<button
|
|
1112
1227
|
className="ghost undo"
|
|
1113
1228
|
disabled={!latest || busy || Boolean(state.decisionLock)}
|
|
1114
|
-
title={state.decisionLock || t(
|
|
1229
|
+
title={state.decisionLock || [t('Undo revert'), shortcuts.undo.map(bindingLabel).join(' / ')].filter(Boolean).join(' · ')}
|
|
1230
|
+
aria-keyshortcuts={shortcuts.undo.join(' ')}
|
|
1115
1231
|
onClick={() => void undo()}
|
|
1116
1232
|
>
|
|
1117
1233
|
{t("Undo revert")}
|
|
@@ -1122,7 +1238,7 @@ export function ReviewApp({
|
|
|
1122
1238
|
{detail && !history && !changes && (
|
|
1123
1239
|
<button
|
|
1124
1240
|
className="ghost history"
|
|
1125
|
-
title={t("
|
|
1241
|
+
title={t("Review history")}
|
|
1126
1242
|
onClick={() => {
|
|
1127
1243
|
setHistory(true);
|
|
1128
1244
|
if (current < 0 && stops.length) setCurrent(0);
|
|
@@ -1134,7 +1250,7 @@ export function ReviewApp({
|
|
|
1134
1250
|
<button
|
|
1135
1251
|
className="ghost round"
|
|
1136
1252
|
aria-label={t("Keyboard shortcuts")}
|
|
1137
|
-
onClick={
|
|
1253
|
+
onClick={showHelp}
|
|
1138
1254
|
>
|
|
1139
1255
|
?
|
|
1140
1256
|
</button>
|
|
@@ -1145,7 +1261,7 @@ export function ReviewApp({
|
|
|
1145
1261
|
<label>
|
|
1146
1262
|
{t("History")}{" "}
|
|
1147
1263
|
<select
|
|
1148
|
-
aria-label={t("Review
|
|
1264
|
+
aria-label={t("Review history")}
|
|
1149
1265
|
value={detail.round}
|
|
1150
1266
|
onChange={(e) => {
|
|
1151
1267
|
location.href = withLanguage(
|
|
@@ -1160,8 +1276,10 @@ export function ReviewApp({
|
|
|
1160
1276
|
.reverse()
|
|
1161
1277
|
.map((round) => (
|
|
1162
1278
|
<option key={round} value={round}>
|
|
1163
|
-
{t("
|
|
1164
|
-
|
|
1279
|
+
{round === state?.latest ? t("Current changes") : (() => {
|
|
1280
|
+
const published = state?.history?.find(entry => entry.number === round)?.created;
|
|
1281
|
+
return published ? new Intl.DateTimeFormat(locale(), { dateStyle: "medium", timeStyle: "medium" }).format(new Date(published * 1000)) : t("Earlier changes");
|
|
1282
|
+
})()}
|
|
1165
1283
|
</option>
|
|
1166
1284
|
))}
|
|
1167
1285
|
</select>
|
|
@@ -1208,6 +1326,68 @@ export function ReviewApp({
|
|
|
1208
1326
|
)}
|
|
1209
1327
|
<div className="review-body">
|
|
1210
1328
|
<main id="paper" ref={paper} tabIndex={-1} onScroll={onScroll}>
|
|
1329
|
+
{changes && detail && !count.pending && (count.accepted > 0 || !stops.length) && (
|
|
1330
|
+
<div className="review-complete" role="status" tabIndex={-1}>
|
|
1331
|
+
<strong>
|
|
1332
|
+
{count.accepted
|
|
1333
|
+
? t("All changes are staged")
|
|
1334
|
+
: t("No uncommitted changes")}
|
|
1335
|
+
</strong>
|
|
1336
|
+
<span>
|
|
1337
|
+
{count.accepted
|
|
1338
|
+
? t("Commit them when you are ready.")
|
|
1339
|
+
: t("The PDF matches the last commit.")}
|
|
1340
|
+
</span>
|
|
1341
|
+
<div className="review-complete-actions" />
|
|
1342
|
+
</div>
|
|
1343
|
+
)}
|
|
1344
|
+
{!changes && detail && !history && !count.pending && (
|
|
1345
|
+
<div className="review-complete" role="status" tabIndex={-1}>
|
|
1346
|
+
<strong>
|
|
1347
|
+
{t(
|
|
1348
|
+
state?.workspace?.status === "waiting_input"
|
|
1349
|
+
? "Agent needs your input"
|
|
1350
|
+
: state?.workspace?.status === "queued"
|
|
1351
|
+
? "Feedback queued"
|
|
1352
|
+
: state?.workspace?.status === "running"
|
|
1353
|
+
? "Feedback sent · Agent is working"
|
|
1354
|
+
: state?.workspace?.status === "building" ||
|
|
1355
|
+
state?.state === "building"
|
|
1356
|
+
? "Preparing the next review…"
|
|
1357
|
+
: "All changes reviewed",
|
|
1358
|
+
)}
|
|
1359
|
+
</strong>
|
|
1360
|
+
<span>
|
|
1361
|
+
{t(
|
|
1362
|
+
state?.workspace?.status === "waiting_input"
|
|
1363
|
+
? "Open Chat to answer the agent's question."
|
|
1364
|
+
: ["running", "queued", "building"].includes(
|
|
1365
|
+
state?.workspace?.status || "",
|
|
1366
|
+
) || state?.state === "building"
|
|
1367
|
+
? "New changes will appear here when ready."
|
|
1368
|
+
: state?.workspace?.comments
|
|
1369
|
+
? "Your comments are saved. Send feedback so the agent can revise."
|
|
1370
|
+
: state?.workspace?.deferred
|
|
1371
|
+
? state.workspace.accepted
|
|
1372
|
+
? "Accepted changes are saved in this task. Choose where to save them below."
|
|
1373
|
+
: state.workspace.destination === "branch"
|
|
1374
|
+
? "Accepted changes are committed to the task branch."
|
|
1375
|
+
: state.workspace.destination === "workspace"
|
|
1376
|
+
? "Accepted changes are in your manuscript."
|
|
1377
|
+
: "There are no accepted changes to save."
|
|
1378
|
+
: state?.workspace?.accepted
|
|
1379
|
+
? "Accepted changes are in your manuscript."
|
|
1380
|
+
: state?.workspace?.unsent && !count.accepted
|
|
1381
|
+
? "Your decisions are saved. Send them to the agent to continue."
|
|
1382
|
+
: count.commented && !state?.workspace
|
|
1383
|
+
? "Your comments are saved for the agent."
|
|
1384
|
+
: "There are no new changes to review. Previous decisions are available in History.",
|
|
1385
|
+
)}
|
|
1386
|
+
</span>
|
|
1387
|
+
{/* A host such as Papilot puts its next step here, for example a commit. */}
|
|
1388
|
+
<div className="review-complete-actions" />
|
|
1389
|
+
</div>
|
|
1390
|
+
)}
|
|
1211
1391
|
{(state?.commentLock || state?.decisionLock) && (
|
|
1212
1392
|
<div className="banner" id="review-lock" role="status">
|
|
1213
1393
|
{t((state.commentLock || state.decisionLock)!)}
|
|
@@ -1240,15 +1420,15 @@ export function ReviewApp({
|
|
|
1240
1420
|
"Updating the comparison with your latest edits. You can keep staging and reverting meanwhile.",
|
|
1241
1421
|
)
|
|
1242
1422
|
: t(
|
|
1243
|
-
"
|
|
1423
|
+
"Updating changes…",
|
|
1244
1424
|
)}
|
|
1245
1425
|
</div>
|
|
1246
1426
|
)}
|
|
1247
1427
|
{!changes && detail && state?.latest && detail.round !== state.latest && (
|
|
1248
1428
|
<div className="banner">
|
|
1249
|
-
{t("
|
|
1429
|
+
{t("Viewing earlier changes.")}{" "}
|
|
1250
1430
|
<a href={withLanguage(`?task=${encodeURIComponent(task)}`)}>
|
|
1251
|
-
{t("
|
|
1431
|
+
{t("Back to current changes →")}
|
|
1252
1432
|
</a>
|
|
1253
1433
|
</div>
|
|
1254
1434
|
)}
|
|
@@ -1266,60 +1446,19 @@ export function ReviewApp({
|
|
|
1266
1446
|
</details>
|
|
1267
1447
|
</div>
|
|
1268
1448
|
) : null}
|
|
1269
|
-
{
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
{
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
</div>
|
|
1283
|
-
)}
|
|
1284
|
-
{!changes && detail && !history && !count.pending && (
|
|
1285
|
-
<div className="review-complete" role="status">
|
|
1286
|
-
<strong>
|
|
1287
|
-
{t(
|
|
1288
|
-
state?.workspace?.status === "waiting_input"
|
|
1289
|
-
? "Agent needs your input"
|
|
1290
|
-
: state?.workspace?.status === "queued"
|
|
1291
|
-
? "Feedback queued"
|
|
1292
|
-
: state?.workspace?.status === "running"
|
|
1293
|
-
? "Feedback sent · Agent is working"
|
|
1294
|
-
: state?.workspace?.status === "building" ||
|
|
1295
|
-
state?.state === "building"
|
|
1296
|
-
? "Preparing the next review…"
|
|
1297
|
-
: "All changes reviewed",
|
|
1298
|
-
)}
|
|
1299
|
-
</strong>
|
|
1300
|
-
<span>
|
|
1301
|
-
{t(
|
|
1302
|
-
state?.workspace?.status === "waiting_input"
|
|
1303
|
-
? "Open Chat to answer the agent's question."
|
|
1304
|
-
: ["running", "queued", "building"].includes(
|
|
1305
|
-
state?.workspace?.status || "",
|
|
1306
|
-
) || state?.state === "building"
|
|
1307
|
-
? "New changes will appear here when ready."
|
|
1308
|
-
: state?.workspace?.comments
|
|
1309
|
-
? "Your comments are saved. Send feedback so the agent can revise."
|
|
1310
|
-
: state?.workspace?.accepted
|
|
1311
|
-
? "Accepted changes are in your manuscript."
|
|
1312
|
-
: state?.workspace?.unsent && !count.accepted
|
|
1313
|
-
? "Your decisions are saved. Send them to the agent to continue."
|
|
1314
|
-
: count.commented && !state?.workspace
|
|
1315
|
-
? "Your comments are saved for the agent."
|
|
1316
|
-
: "There are no new changes to review. Previous decisions are available in History.",
|
|
1317
|
-
)}
|
|
1318
|
-
</span>
|
|
1319
|
-
{/* A host such as Papilot puts its next step here, for example a commit. */}
|
|
1320
|
-
<div className="review-complete-actions" />
|
|
1321
|
-
</div>
|
|
1322
|
-
)}
|
|
1449
|
+
{detail?.references &&
|
|
1450
|
+
(detail.references.entries.length > 0 ||
|
|
1451
|
+
detail.references.missing.length > 0) && (
|
|
1452
|
+
<ReferenceBanner
|
|
1453
|
+
references={detail.references}
|
|
1454
|
+
checks={references.checks}
|
|
1455
|
+
pending={references.pending}
|
|
1456
|
+
confirmed={references.confirmed}
|
|
1457
|
+
changeOf={(unit) => (unit ? (firstStop.get(unit) ?? -1) : -1)}
|
|
1458
|
+
onSelect={(index) => select(index)}
|
|
1459
|
+
onRetry={references.retry}
|
|
1460
|
+
/>
|
|
1461
|
+
)}
|
|
1323
1462
|
<div className="document">
|
|
1324
1463
|
{detail ? (
|
|
1325
1464
|
<>
|
|
@@ -1356,16 +1495,14 @@ export function ReviewApp({
|
|
|
1356
1495
|
))}
|
|
1357
1496
|
{detail.loose.length > 0 && (
|
|
1358
1497
|
<section className="part">
|
|
1359
|
-
|
|
1360
|
-
{
|
|
1361
|
-
|
|
1362
|
-
: t("Changes")}
|
|
1363
|
-
</h2>
|
|
1498
|
+
{detail.sections.length > 0 && (
|
|
1499
|
+
<h2 className="part-title">{t("Other changes")}</h2>
|
|
1500
|
+
)}
|
|
1364
1501
|
{detail.loose.map((block) => change(block, index++, false))}
|
|
1365
1502
|
</section>
|
|
1366
1503
|
)}
|
|
1367
1504
|
{!stops.length && !changes && (
|
|
1368
|
-
<p className="empty">{t("
|
|
1505
|
+
<p className="empty">{t("No changes to review.")}</p>
|
|
1369
1506
|
)}
|
|
1370
1507
|
</>
|
|
1371
1508
|
) : error ? (
|
|
@@ -1442,6 +1579,18 @@ export function ReviewApp({
|
|
|
1442
1579
|
{toast}
|
|
1443
1580
|
</div>
|
|
1444
1581
|
)}
|
|
1582
|
+
{confirm && (
|
|
1583
|
+
<ConfirmReferences
|
|
1584
|
+
items={confirm.items}
|
|
1585
|
+
staging={changes}
|
|
1586
|
+
onCancel={() => setConfirm(null)}
|
|
1587
|
+
onConfirm={() => {
|
|
1588
|
+
const chosen = confirm;
|
|
1589
|
+
setConfirm(null);
|
|
1590
|
+
void decide(chosen.kind, chosen.chosen, chosen.body, true);
|
|
1591
|
+
}}
|
|
1592
|
+
/>
|
|
1593
|
+
)}
|
|
1445
1594
|
{help && (
|
|
1446
1595
|
<div className="help-backdrop" onClick={() => setHelp(false)}>
|
|
1447
1596
|
<dialog
|
|
@@ -1453,26 +1602,28 @@ export function ReviewApp({
|
|
|
1453
1602
|
<dl>
|
|
1454
1603
|
{(changes
|
|
1455
1604
|
? [
|
|
1456
|
-
[
|
|
1457
|
-
[
|
|
1458
|
-
[
|
|
1459
|
-
[
|
|
1460
|
-
[
|
|
1461
|
-
[
|
|
1605
|
+
[shortcuts.accept.map(bindingLabel).join(' / '), "Stage or unstage"],
|
|
1606
|
+
[shortcuts.reject.map(bindingLabel).join(' / '), "Revert"],
|
|
1607
|
+
[shortcuts.next.map(bindingLabel).join(' / '), "Next change"],
|
|
1608
|
+
[shortcuts.previous.map(bindingLabel).join(' / '), "Previous change"],
|
|
1609
|
+
[shortcuts.pending.map(bindingLabel).join(' / '), "Next unstaged"],
|
|
1610
|
+
[shortcuts.source.map(bindingLabel).join(' / '), "Show LaTeX source"],
|
|
1611
|
+
[["⌘Z / Ctrl+Z", ...shortcuts.undo.map(bindingLabel)].join(' / '), "Undo revert"],
|
|
1462
1612
|
["⌘C / Ctrl+C", "Copy selected PDF text"],
|
|
1463
1613
|
]
|
|
1464
1614
|
: [
|
|
1465
|
-
[
|
|
1466
|
-
[
|
|
1467
|
-
[
|
|
1468
|
-
[
|
|
1469
|
-
[
|
|
1470
|
-
[
|
|
1471
|
-
[
|
|
1615
|
+
[shortcuts.accept.map(bindingLabel).join(' / '), "Accept"],
|
|
1616
|
+
[shortcuts.reject.map(bindingLabel).join(' / '), "Reject"],
|
|
1617
|
+
[shortcuts.comment.map(bindingLabel).join(' / '), "Comment"],
|
|
1618
|
+
[shortcuts.next.map(bindingLabel).join(' / '), "Next change"],
|
|
1619
|
+
[shortcuts.previous.map(bindingLabel).join(' / '), "Previous change"],
|
|
1620
|
+
[shortcuts.pending.map(bindingLabel).join(' / '), "Next undecided"],
|
|
1621
|
+
[shortcuts.source.map(bindingLabel).join(' / '), "Show LaTeX source"],
|
|
1622
|
+
[["⌘Z / Ctrl+Z", ...shortcuts.undo.map(bindingLabel)].join(' / '), "Undo"],
|
|
1472
1623
|
["⌘C / Ctrl+C", "Copy selected PDF text"],
|
|
1473
1624
|
]
|
|
1474
1625
|
).map(([key, label]) => (
|
|
1475
|
-
<Fragment key={
|
|
1626
|
+
<Fragment key={label}>
|
|
1476
1627
|
<dt>
|
|
1477
1628
|
<kbd>{t(key)}</kbd>
|
|
1478
1629
|
</dt>
|