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
package/dist/index.html
CHANGED
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
|
9
9
|
document.querySelector('meta[name="theme-color"]').content = dark ? '#26272b' : '#ffffff';
|
|
10
10
|
})();
|
|
11
|
-
</script><style>html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;background:#fff}html[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#26272b}</style> <script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
</script><style>html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;background:#fff}html[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#26272b}</style> <script type="module" crossorigin src="/assets/index-D_VX51no.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cc-awdNn.css">
|
|
13
13
|
</head><body><div id="root"></div></body></html>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
Papilot is a personal workspace for revising \LaTeX{} papers with an AI agent
|
|
13
13
|
and reviewing every resulting change as a PDF difference before it reaches the
|
|
14
14
|
manuscript. Each editing task runs in an isolated Git worktree, may be driven
|
|
15
|
-
by Codex, Claude or
|
|
15
|
+
by Codex, Claude, Antigravity or Kimi Code, and proceeds in review rounds in which the
|
|
16
16
|
author accepts, rejects or comments on individual changes. This paper describes
|
|
17
17
|
the system through a sequence of small formal models. We define edits and
|
|
18
18
|
review rounds as operators on a document, prove that compatible edits commute
|
|
@@ -91,7 +91,8 @@ remote (\cref{sec:git}).
|
|
|
91
91
|
\subsection{Agents}
|
|
92
92
|
|
|
93
93
|
Each task runs on one agent: Codex through its app server, Claude through the
|
|
94
|
-
Claude Agent SDK,
|
|
94
|
+
Claude Agent SDK, Antigravity through its command line, or Kimi Code through
|
|
95
|
+
ACP. For Kimi, install the CLI and run \texttt{kimi login}. Every agent uses its
|
|
95
96
|
own login on the host, and an agent that is not installed or signed in is listed
|
|
96
97
|
as unavailable. When an agent reports usage, the new-task page shows each usage
|
|
97
98
|
window with how much is used and when it resets (\cref{sec:usage}).
|
|
@@ -124,6 +125,7 @@ Variable & Purpose \\
|
|
|
124
125
|
\texttt{PAPILOT\_DATA} & state and generated example, \texttt{\textasciitilde/.papilot} by default \\
|
|
125
126
|
\texttt{PAPILOT\_CODEX\_BIN} & Codex executable \\
|
|
126
127
|
\texttt{ANTHROPIC\_API\_KEY} & API key for Claude tasks \\
|
|
128
|
+
\texttt{PAPILOT\_KIMI\_BIN} & Kimi Code executable \\
|
|
127
129
|
\texttt{PAPILOT\_AGY\_BIN} & Antigravity command line \\
|
|
128
130
|
\texttt{PAPILOT\_AGY\_PROXY} & HTTP proxy used only for Antigravity \\
|
|
129
131
|
\texttt{PAPILOT\_REVIEW\_ENGINE} & review engine directory \\
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
\section{Agents and scheduling}
|
|
2
2
|
\label{sec:agents}
|
|
3
3
|
|
|
4
|
-
\subsection{One protocol for
|
|
4
|
+
\subsection{One protocol for every agent}
|
|
5
5
|
|
|
6
6
|
Every agent is driven through the same small protocol: a thread is started or
|
|
7
7
|
resumed, a turn is started with the author's text, the agent streams items
|
|
@@ -40,8 +40,9 @@ task in a recoverable state with a \emph{Check status} action (not shown).}
|
|
|
40
40
|
\end{figure}
|
|
41
41
|
|
|
42
42
|
Codex speaks this protocol natively. Claude runs through the Claude Agent SDK,
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Antigravity runs the \texttt{agy} command line once per turn, and Kimi Code
|
|
44
|
+
runs \texttt{kimi acp}; adapters translate their interfaces into the same
|
|
45
|
+
requests and notifications. Kimi sessions resume after a restart. Each
|
|
45
46
|
agent uses its own login on the host, and Papilot never reads, stores or
|
|
46
47
|
forwards a credential. An agent that cannot ask for permission, such as a
|
|
47
48
|
headless command line, is told in advance which tools it may not use.
|
|
@@ -102,5 +103,7 @@ answers after a mean delay $\Theta$, the expected wall-clock time of the turn is
|
|
|
102
103
|
\E[T] = T_w \bigl(1 + \zeta \Theta\bigr),
|
|
103
104
|
\label{eq:approvals}
|
|
104
105
|
\end{equation}
|
|
105
|
-
which
|
|
106
|
-
|
|
106
|
+
which motivates reducing unnecessary approval requests. Codex and Claude can
|
|
107
|
+
run sandboxed commands under their permission policies; Kimi uses manual
|
|
108
|
+
approvals for tools on the host, without a Papilot sandbox. Antigravity edits
|
|
109
|
+
files but cannot run shell commands in Papilot.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "papilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A Cursor-like workspace for your papers: annotate and review PDFs directly without ever touching LaTeX.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"latex",
|
|
@@ -12,17 +12,19 @@
|
|
|
12
12
|
"codex",
|
|
13
13
|
"claude",
|
|
14
14
|
"antigravity",
|
|
15
|
+
"kimi",
|
|
16
|
+
"kimi-code",
|
|
15
17
|
"writing",
|
|
16
18
|
"academic",
|
|
17
19
|
"manuscript"
|
|
18
20
|
],
|
|
19
|
-
"homepage": "https://
|
|
21
|
+
"homepage": "https://papilot.ai",
|
|
20
22
|
"repository": {
|
|
21
23
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/
|
|
24
|
+
"url": "git+https://github.com/PapilotAI/papilot.git"
|
|
23
25
|
},
|
|
24
26
|
"bugs": {
|
|
25
|
-
"url": "https://github.com/
|
|
27
|
+
"url": "https://github.com/PapilotAI/papilot/issues"
|
|
26
28
|
},
|
|
27
29
|
"license": "MIT",
|
|
28
30
|
"author": "Cui Chen",
|
|
@@ -34,9 +36,13 @@
|
|
|
34
36
|
"bin",
|
|
35
37
|
"src/server",
|
|
36
38
|
"src/shared.ts",
|
|
39
|
+
"src/skill-icons.ts",
|
|
40
|
+
"src/skill-metadata.ts",
|
|
37
41
|
"src/notes.ts",
|
|
42
|
+
"src/note-groups.ts",
|
|
38
43
|
"src/conversation.ts",
|
|
39
44
|
"src/commit.ts",
|
|
45
|
+
"src/task-caption.ts",
|
|
40
46
|
"skills",
|
|
41
47
|
"dist",
|
|
42
48
|
"scripts/demo.ts",
|
|
@@ -62,9 +68,9 @@
|
|
|
62
68
|
"doctor": "node bin/papilot.mjs doctor",
|
|
63
69
|
"service:install": "tsx scripts/service.ts install",
|
|
64
70
|
"service:stop": "tsx scripts/service.ts stop",
|
|
65
|
-
"test:ui": "npm run build && node scripts/check-ui.mjs && node scripts/check-review-notes.mjs && node --import tsx scripts/check-review-flow.mjs && node scripts/check-skills.mjs && node --import tsx scripts/check-accounts.mjs",
|
|
71
|
+
"test:ui": "npm run build && node scripts/check-ui.mjs && node scripts/check-appearance.mjs && node scripts/check-updates.mjs && node scripts/check-papers.mjs && node scripts/check-imports.mjs && node scripts/check-compilation.mjs && node scripts/check-build-problems.mjs && node scripts/check-review-notes.mjs && node --import tsx scripts/check-note-groups.mjs && node scripts/check-note-selection.mjs && node scripts/check-ai-review.mjs && node --import tsx scripts/check-note-results.mjs && node --import tsx scripts/check-review-flow.mjs && node scripts/check-skills.mjs && node --import tsx scripts/check-skill-library.mjs && node scripts/check-attachments.mjs && node --import tsx scripts/check-accounts.mjs",
|
|
66
72
|
"site": "node scripts/build-site.mjs",
|
|
67
|
-
"site
|
|
73
|
+
"test:site": "npm run site && node scripts/check-site.mjs",
|
|
68
74
|
"prepack": "npm run build && npm run setup:review"
|
|
69
75
|
},
|
|
70
76
|
"dependencies": {
|
|
@@ -76,7 +82,8 @@
|
|
|
76
82
|
"fastify": "^5.12.7",
|
|
77
83
|
"mupdf": "^1.28.1",
|
|
78
84
|
"proper-lockfile": "^4.1.2",
|
|
79
|
-
"tsx": "^4.23.15"
|
|
85
|
+
"tsx": "^4.23.15",
|
|
86
|
+
"yauzl": "^3.4.0"
|
|
80
87
|
},
|
|
81
88
|
"devDependencies": {
|
|
82
89
|
"@anthropic-ai/claude-agent-sdk": "^0.3.311",
|
|
@@ -85,6 +92,7 @@
|
|
|
85
92
|
"@types/node": "^22.20.5",
|
|
86
93
|
"@types/react": "^19.3.0",
|
|
87
94
|
"@types/react-dom": "^19.3.0",
|
|
95
|
+
"@types/yauzl": "^3.4.0",
|
|
88
96
|
"@vitejs/plugin-react": "^6.1.2",
|
|
89
97
|
"katex": "^0.16.47",
|
|
90
98
|
"pdfjs-dist": "^6.4.299",
|
package/scripts/service.ts
CHANGED
|
@@ -22,6 +22,7 @@ if (process.argv[2] === 'stop') {
|
|
|
22
22
|
} catch { /* An older or incomplete plist may not contain environment settings. */ }
|
|
23
23
|
}
|
|
24
24
|
const env = serviceEnvironment(ROOT, os.homedir(), process.execPath, process.env, previous);
|
|
25
|
+
env.PAPILOT_SERVICE_MANAGER = 'launchd';
|
|
25
26
|
const args = ['/usr/bin/caffeinate', '-i', process.execPath, '--import', 'tsx', path.join(ROOT, 'src/server/main.ts')];
|
|
26
27
|
const plist = `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>
|
|
27
28
|
<key>Label</key><string>${label}</string><key>ProgramArguments</key><array>${args.map(a => `<string>${xml(a)}</string>`).join('')}</array>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AI review
|
|
3
|
+
description: Read the paper and leave PDF comments without editing the manuscript
|
|
4
|
+
hint: What should the reviewer focus on? Leave empty for the whole paper
|
|
5
|
+
order: 8
|
|
6
|
+
output: annotations
|
|
7
|
+
---
|
|
8
|
+
Review the paper as a constructive academic reviewer. Read it and leave suggestions; do not edit, rewrite, commit, or apply changes to the manuscript.
|
|
9
|
+
|
|
10
|
+
- Follow the author's requested focus, or review the whole paper when none is given.
|
|
11
|
+
- Look for unsupported claims, gaps in reasoning, unclear definitions, inconsistent terminology, and concrete writing issues.
|
|
12
|
+
- Anchor each comment to the smallest relevant passage in the supplied PDF: the specific sentence, claim, or equation at issue, usually 1–3 lines. Avoid whole-paragraph or page-level highlights. If the issue concerns a symbol within a line, name that symbol in the comment.
|
|
13
|
+
- Write one issue per comment, usually in one short sentence: aim for 10–30 English words or 20–60 Chinese characters. State the problem and a concrete next step; add a second short sentence only when needed for accuracy. Example: “This bound is undefined at p = 1; state that case separately.”
|
|
14
|
+
- Omit praise, preambles, repeated quotations, background explanations, full derivations, and rewritten paragraphs. For a complex issue, identify the failing step and needed clarification; leave extended reasoning for follow-up.
|
|
15
|
+
- Prefer a few high-impact, actionable comments to minor or speculative criticism. Flag a repeated issue once at a representative location. Do not invent sources, results, or assumptions, or claim a citation is wrong without evidence.
|
|
16
|
+
- Use the author's requested language, otherwise the manuscript's language.
|
|
17
|
+
- The supplied PDF extraction contains text, equations, and captions, not figure images. Do not claim to have inspected page appearance.
|
|
18
|
+
- Return PDF annotations in the format Papilot supplies, with a single brief sentence summarizing the main priority. Do not repeat the findings in the summary or add a review essay. The author decides which suggestions to use in a separate editing task.
|
package/skills/latex-warnings.md
CHANGED
|
@@ -6,7 +6,7 @@ order: 6
|
|
|
6
6
|
---
|
|
7
7
|
Fix the problems LaTeX reports when it builds the paper.
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- When the request lists problems from the last compilation, with their files and lines, fix those. Otherwise compile in your worktree (for example with `latexmk`) and read the log. If you cannot run commands, look for the same problems in the source instead.
|
|
10
10
|
- Fix undefined or duplicate labels, references and citations; overfull boxes, by rewording slightly or allowing a break where the line overflows; and package warnings that point to real mistakes.
|
|
11
11
|
- Do not silence warnings globally: no document-wide `\sloppy`, badness or tolerance changes, and no new packages, unless the request asks for them.
|
|
12
12
|
- In your summary, list the warnings you fixed and any you left, with the reason.
|
package/src/conversation.ts
CHANGED
|
@@ -6,6 +6,12 @@ export const reviewInstructions = 'Continue this task using the decisions and co
|
|
|
6
6
|
// Earlier versions stored the prompt itself as the author's message.
|
|
7
7
|
export const isFeedbackPrompt = (text: string) => text === reviewInstructions || text === 'Address the comments from this review round. Preserve accepted changes and do not reintroduce rejected edits.';
|
|
8
8
|
|
|
9
|
+
// Older AI reviews appended a generated copy of every PDF suggestion to the
|
|
10
|
+
// chat summary. Keep those records intact, but display only their summary.
|
|
11
|
+
export function aiReviewSummary(text: string) {
|
|
12
|
+
return text.split(/\n\n1\. \*\*Page \d+\*\* — /, 1)[0].trimEnd();
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
// What the agent receives when its own edits stop the review build. The chat
|
|
10
16
|
// shows the error, never this prompt.
|
|
11
17
|
export function buildFixPrompt(failure: BuildFailure) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const NOTE_GROUPS = ['yellow', 'green', 'purple', 'orange', 'red'] as const;
|
|
2
|
+
// Older saved annotations and review drafts keep their identity; blue is now
|
|
3
|
+
// displayed and described as yellow, and is no longer offered in the palette.
|
|
4
|
+
export type NoteGroup = typeof NOTE_GROUPS[number] | 'blue';
|
|
5
|
+
export type GroupInstructions = Partial<Record<typeof NOTE_GROUPS[number], string>>;
|
|
6
|
+
export const canonicalGroup = (group: NoteGroup): typeof NOTE_GROUPS[number] => group === 'blue' ? 'yellow' : group;
|
|
7
|
+
export const isNoteGroup = (value: unknown): value is NoteGroup => value === 'blue' || NOTE_GROUPS.includes(value as typeof NOTE_GROUPS[number]);
|
|
8
|
+
export function parseGroupInstructions(value: unknown): GroupInstructions {
|
|
9
|
+
if (value === undefined) return {};
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Use valid instructions for each color group.');
|
|
11
|
+
const result: GroupInstructions = {};
|
|
12
|
+
for (const [key, text] of Object.entries(value)) {
|
|
13
|
+
if (!isNoteGroup(key) || typeof text !== 'string' || text.length > 4000) throw new Error('Use valid instructions for each color group (up to 4000 characters).');
|
|
14
|
+
const group = canonicalGroup(key);
|
|
15
|
+
if (result[group] && result[group] !== text.trim()) throw new Error('Use one instruction for each color group.');
|
|
16
|
+
if (text.trim()) result[group] = text.trim();
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export const groupsWithoutInstructions = (notes: { group?: NoteGroup; comment: string }[], instructions: GroupInstructions) =>
|
|
21
|
+
NOTE_GROUPS.filter(group => !instructions[group]?.trim() && notes.some(note => note.group && canonicalGroup(note.group) === group && !note.comment.trim()));
|
|
22
|
+
export const GROUP_COLORS: Record<NoteGroup, string> = { yellow: '#eab308', blue: '#eab308', green: '#16a34a', purple: '#a855f7', orange: '#ea860c', red: '#e05260' };
|
|
23
|
+
export const GROUP_NAMES: Record<NoteGroup, string> = { yellow: 'Yellow', blue: 'Yellow', green: 'Green', purple: 'Purple', orange: 'Orange', red: 'Red' };
|
|
24
|
+
export function noteGroups<T extends { group?: NoteGroup }>(notes: T[]) {
|
|
25
|
+
return NOTE_GROUPS.flatMap(group => {
|
|
26
|
+
const entries = notes.flatMap((note, index) => note.group && canonicalGroup(note.group) === group ? [{ note, index }] : []);
|
|
27
|
+
return entries.length ? [{ group, entries }] : [];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export const GROUP_INSTRUCTIONS = 'Color groups are labels only, with no built-in meaning. Apply the author\'s instructions for this message to every marked location in each named color group. Never infer an editing action from a color. If a group\'s intended treatment is unclear, ask the author.';
|
package/src/notes.ts
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { canonicalGroup, GROUP_INSTRUCTIONS, GROUP_NAMES, NOTE_GROUPS, type GroupInstructions } from './note-groups.ts';
|
|
2
|
+
import type { Annotation, NoteAnchor, NoteSummary } from './shared.ts';
|
|
3
|
+
export type { NoteSummary } from './shared.ts';
|
|
2
4
|
|
|
3
5
|
// PDF notes reach the agent as one plain-text message. The chat shows that
|
|
4
6
|
// message as the notes again, so the format and its reader live together.
|
|
5
7
|
const PREAMBLE = 'Revise the manuscript according to these notes the author marked on the compiled PDF.';
|
|
6
8
|
const QUOTE = ' PDF text: "', REGION = ' Marked region: a figure, equation or other non-text area', NOTE = '\n Note: ';
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
const noteWhere = (note: Annotation) => [`Page ${note.page + 1}`, note.source ? `${note.source.file} ${note.source.lines[0] === note.source.lines[1] ? `line ${note.source.lines[0]}` : `lines ${note.source.lines[0]}–${note.source.lines[1]}`}` : '', note.author ? `from ${note.author.name}` : ''].filter(Boolean).join(' · ');
|
|
11
|
+
export const summarizeNotes = (notes: Annotation[]): NoteSummary[] => notes.map(note => ({ ...(note.group ? { group: note.group } : {}), id: note.id, where: noteWhere(note), quote: note.quote, comment: note.comment, anchor: { sources: note.source ? [{ file: note.source.file, lines: note.source.lines }] : [] } }));
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// Also recover anchors from older messages, whose locations were only text.
|
|
14
|
+
export function noteAnchor(note: NoteSummary): NoteAnchor {
|
|
15
|
+
if (note.anchor) return note.anchor;
|
|
16
|
+
const round = note.where.match(/^Round (\d+)\b/), sources: NoteAnchor['sources'] = [];
|
|
17
|
+
for (const place of note.where.split(/ · |; /)) {
|
|
18
|
+
const match = place.match(/^(.+?)(?::| lines? )(\d+(?:[–-]\d+)?(?:,\s*\d+(?:[–-]\d+)?)*)$/);
|
|
19
|
+
if (!match) continue;
|
|
20
|
+
for (const range of match[2].split(/,\s*/)) {
|
|
21
|
+
const [start, end] = range.split(/[–-]/).map(Number);
|
|
22
|
+
sources.push({ file: match[1], lines: [start, end ?? start] });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { sources, ...(round ? { round: Number(round[1]) } : {}) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function reviewNotesInstructions(notes: NoteSummary[], request = '') {
|
|
29
|
+
const items = notes.map((note, index) => `${index + 1}. ${note.where}${note.group ? ` · Color group: ${GROUP_NAMES[note.group]}` : ''}\n${note.quote ? `${QUOTE}${note.quote}"` : REGION}${NOTE}${note.comment.trim().replace(/\n/g, '\n ')}`);
|
|
30
|
+
return `${PREAMBLE}${notes.some(note => note.group) ? ` ${GROUP_INSTRUCTIONS}` : ''}${request.trim() ? ` Overall request: ${request.trim()}\n\n` : ''} These notes refer to the displayed review round and its Before, After or Unchanged text. Region coordinates are percentages of the displayed PDF strip (left, top, right, bottom); any quoted text for a region is nearby context. Read the current source before editing. Preserve accepted changes and do not reintroduce rejected edits unless a note explicitly asks for it.\n\n${items.join('\n\n')}`;
|
|
13
31
|
}
|
|
14
32
|
|
|
15
|
-
export function notesInstructions(notes: Annotation[]) {
|
|
33
|
+
export function notesInstructions(notes: Annotation[], request = '', groupInstructions: GroupInstructions = {}) {
|
|
16
34
|
const items = notes.map((note, index) => {
|
|
17
|
-
const where =
|
|
35
|
+
const where = noteWhere(note);
|
|
18
36
|
const source = note.source?.excerpt ? [' LaTeX source of the marked region:', ' ```latex', ...note.source.excerpt.split('\n').map(line => ' ' + line), ' ```'] : [];
|
|
19
|
-
return [`${index + 1}. ${where}`, ...source, note.quote ? `${QUOTE}${note.quote}"` : REGION, `${NOTE.slice(1)}${note.comment.trim().replace(/\n/g, '\n ')}`].join('\n');
|
|
37
|
+
return [`${index + 1}. ${where}${note.group ? ` · Color group: ${GROUP_NAMES[note.group]}` : ''}`, ...source, note.quote ? `${QUOTE}${note.quote}"` : REGION, `${NOTE.slice(1)}${note.comment.trim().replace(/\n/g, '\n ')}`].join('\n');
|
|
20
38
|
});
|
|
21
|
-
|
|
39
|
+
const scope = request.trim() ? `follow both the overall request and the notes.\n\nOverall request:\n${request.trim()}` : 'change only what the notes ask for.';
|
|
40
|
+
const groups = NOTE_GROUPS.filter(group => groupInstructions[group]?.trim() && notes.some(note => note.group && canonicalGroup(note.group) === group));
|
|
41
|
+
const directions = groups.length ? `\n\nInstructions for color groups (apply only to the attached locations in that group):\n${groups.map(group => `${GROUP_NAMES[group]}:\n ${groupInstructions[group]!.trim().replace(/\n/g, '\n ')}`).join('\n')}` : '';
|
|
42
|
+
return `${PREAMBLE}${notes.some(note => note.group) ? ` ${GROUP_INSTRUCTIONS}` : ''} SyncTeX mapped each marked region to the LaTeX file and lines that produced it; the PDF text is what the author saw. Read the current source before editing, and ${scope}${directions}\n\n${items.join('\n\n')}`;
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
// The notes in a message written by notesInstructions, or null for any other message.
|
|
@@ -39,7 +60,10 @@ export function readNotesInstructions(text: string): NoteSummary[] | null {
|
|
|
39
60
|
if (note < 0) return null;
|
|
40
61
|
const marked = rest.slice(0, note);
|
|
41
62
|
if (marked !== REGION && !(marked.startsWith(QUOTE) && marked.endsWith('"'))) return null;
|
|
42
|
-
|
|
63
|
+
const legacy = heading[1].endsWith(' · Color group: Blue');
|
|
64
|
+
const group = legacy ? 'blue' : NOTE_GROUPS.find(group => heading[1].endsWith(` · Color group: ${GROUP_NAMES[group]}`));
|
|
65
|
+
const where = group ? heading[1].slice(0, -` · Color group: ${legacy ? 'Blue' : GROUP_NAMES[group]}`.length) : heading[1];
|
|
66
|
+
notes.push({ ...(group ? { group } : {}), where, quote: marked === REGION ? '' : marked.slice(QUOTE.length, -1), comment: rest.slice(note + NOTE.length).replace(/\n {3}/g, '\n') });
|
|
43
67
|
}
|
|
44
68
|
return notes.length ? notes : null;
|
|
45
69
|
}
|