promptfigure 0.2.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.
Files changed (42) hide show
  1. package/README.md +95 -0
  2. package/adapters/claude-code/SKILL.md +381 -0
  3. package/adapters/claude-code/install.mjs +9 -0
  4. package/adapters/codex/marketplace.json +14 -0
  5. package/adapters/codex/promptfigure/.codex-plugin/plugin.json +6 -0
  6. package/adapters/codex/promptfigure/skills/promptfigure-local/SKILL.md +381 -0
  7. package/bin/pf.mjs +1953 -0
  8. package/package.json +44 -0
  9. package/scripts/build-adapters.mjs +80 -0
  10. package/scripts/test-e2e.mjs +117 -0
  11. package/skill/promptfigure-local/SKILL.md +381 -0
  12. package/src/anchor.mjs +63 -0
  13. package/src/config.mjs +75 -0
  14. package/src/craft-rules.mjs +158 -0
  15. package/src/craft.mjs +600 -0
  16. package/src/doc/docx.mjs +69 -0
  17. package/src/doc/index.mjs +35 -0
  18. package/src/doc/para.mjs +54 -0
  19. package/src/doc/tex.mjs +161 -0
  20. package/src/doc/texbuild.mjs +158 -0
  21. package/src/docsearch.mjs +96 -0
  22. package/src/entity-pair.mjs +17 -0
  23. package/src/events.mjs +52 -0
  24. package/src/extract.mjs +124 -0
  25. package/src/figure-catalog.mjs +326 -0
  26. package/src/journal.mjs +67 -0
  27. package/src/ledger.mjs +43 -0
  28. package/src/next.mjs +125 -0
  29. package/src/plan.mjs +112 -0
  30. package/src/png-trim.mjs +217 -0
  31. package/src/quality.mjs +325 -0
  32. package/src/ratio.mjs +87 -0
  33. package/src/render.mjs +247 -0
  34. package/src/review.mjs +48 -0
  35. package/src/server.mjs +218 -0
  36. package/src/store.mjs +91 -0
  37. package/src/vectorize.mjs +54 -0
  38. package/tray/pf-tray.py +265 -0
  39. package/web/app.js +613 -0
  40. package/web/index.html +73 -0
  41. package/web/probe.html +36 -0
  42. package/web/style.css +208 -0
package/web/index.html ADDED
@@ -0,0 +1,73 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>promptFigure · 论文配图工作台</title>
7
+ <link rel="stylesheet" href="/style.css">
8
+ </head>
9
+ <body>
10
+ <header id="topbar">
11
+ <div class="brand">prompt<span>Figure</span> <em>本地工作台</em></div>
12
+ <div id="doc-title">加载中…</div>
13
+ <div class="spacer"></div>
14
+ <button id="btn-queue">审批队列 <span id="cnt-queue" class="cnt"></span></button>
15
+ <div id="conn" class="dot" title="实时连接"></div>
16
+ </header>
17
+
18
+ <main>
19
+ <section id="doc-pane">
20
+ <div id="doc-view" class="loading">正在渲染文档…</div>
21
+ </section>
22
+
23
+ <aside id="side">
24
+ <nav id="tabs">
25
+ <button data-tab="review" class="on">审批 <span id="cnt-review" class="cnt"></span></button>
26
+ <button data-tab="figures">图库 <span id="cnt-figures" class="cnt"></span></button>
27
+ <button data-tab="timeline">时间线</button>
28
+ </nav>
29
+ <div id="tab-review" class="tab on"><p class="empty">暂无待审批的图</p></div>
30
+ <div id="tab-figures" class="tab"><p class="empty">还没有图</p></div>
31
+ <div id="tab-timeline" class="tab"><p class="empty">暂无事件</p></div>
32
+ </aside>
33
+ </main>
34
+
35
+ <!-- 图版本对比 / 全屏 / 就地审批 -->
36
+ <div id="overlay" class="hidden">
37
+ <div id="overlay-head">
38
+ <span id="overlay-title"></span>
39
+ <span class="spacer"></span>
40
+ <button id="btn-locate">定位上下文</button>
41
+ <button id="btn-fs">全屏</button>
42
+ <button id="btn-close">关闭 ✕</button>
43
+ </div>
44
+ <div id="overlay-main">
45
+ <div id="overlay-body"></div>
46
+ <aside id="overlay-review"></aside>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- 审批队列:逐张看大图直接批,不用回原文 -->
51
+ <div id="queue" class="hidden">
52
+ <div id="queue-head">
53
+ <b>审批队列</b>
54
+ <span id="queue-pos"></span>
55
+ <span class="spacer"></span>
56
+ <button id="queue-prev">← 上一张</button>
57
+ <button id="queue-next">下一张 →</button>
58
+ <button id="queue-close">关闭 ✕</button>
59
+ </div>
60
+ <div id="queue-main">
61
+ <div id="queue-img"><img id="queue-img-el" alt=""></div>
62
+ <aside id="queue-side"></aside>
63
+ </div>
64
+ </div>
65
+
66
+ <div id="toast" class="hidden"></div>
67
+
68
+ <script src="/vendor/jszip.min.js"></script>
69
+ <script src="/vendor/docx-preview.min.js"></script>
70
+ <script src="/vendor/pdf.min.js"></script>
71
+ <script type="module" src="/app.js"></script>
72
+ </body>
73
+ </html>
package/web/probe.html ADDED
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html><head><meta charset="utf-8"><title>pf probe</title></head>
3
+ <body><pre id="out">boot...</pre>
4
+ <script src="/vendor/pdf.min.js"></script>
5
+ <script>
6
+ const out = [];
7
+ const log = (m) => { out.push(m); document.getElementById("out").textContent = out.join("\n"); };
8
+ window.onerror = (e) => log("window.onerror: " + e.message);
9
+ (async () => {
10
+ try {
11
+ log("pdfjsLib: " + typeof window.pdfjsLib);
12
+ window.pdfjsLib.GlobalWorkerOptions.workerSrc = "/vendor/pdf.worker.min.js";
13
+ const buf = await fetch("/api/file?doc=1b0d6cddb9a8").then((r) => r.arrayBuffer());
14
+ log("pdf bytes: " + buf.byteLength);
15
+ const pdf = await window.pdfjsLib.getDocument({ data: buf }).promise;
16
+ log("pages: " + pdf.numPages);
17
+ const page = await pdf.getPage(1);
18
+ const vp = page.getViewport({ scale: 1.5 });
19
+ log("viewport: " + vp.width + "x" + vp.height);
20
+ const c = document.createElement("canvas");
21
+ c.width = vp.width; c.height = vp.height;
22
+ const t0 = Date.now();
23
+ await page.render({ canvasContext: c.getContext("2d"), viewport: vp }).promise;
24
+ log("render done in " + (Date.now() - t0) + "ms");
25
+ const d = c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
26
+ let dark = 0;
27
+ for (let i = 0; i < d.length; i += 4) if (d[i] < 200) dark++;
28
+ log("darkPixels: " + dark + " / " + (d.length / 4));
29
+ const tc = await page.getTextContent();
30
+ log("textContent items: " + tc.items.length + " | first: " + JSON.stringify((tc.items[0] || {}).str || "").slice(0, 60));
31
+ log("renderTextLayer: " + typeof window.pdfjsLib.renderTextLayer);
32
+ } catch (e) {
33
+ log("ERR: " + (e && (e.stack || e.message) || JSON.stringify(e)));
34
+ }
35
+ })();
36
+ </script></body></html>
package/web/style.css ADDED
@@ -0,0 +1,208 @@
1
+ /* promptFigure 本地工作台 —— 零构建、浅色、只读(唯一可操作是审批) */
2
+ :root {
3
+ --bg: #f6f7f9; --panel: #ffffff; --line: #e4e7ec;
4
+ --ink: #1a2233; --ink2: #667085;
5
+ --accent: #4361ee;
6
+ --yellow: #fff3c2; --yellow-line: #e6b800;
7
+ --green: #d3f2dd; --green-line: #2e9e5b;
8
+ --red: #fde2e2; --red-line: #d64545;
9
+ --orange: #ffe8d1; --orange-line: #e07b1f;
10
+ --gray: #ececec; --gray-line: #b3b3b3;
11
+ }
12
+ * { box-sizing: border-box; margin: 0; }
13
+ html, body { height: 100%; }
14
+ body {
15
+ font: 14px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
16
+ background: var(--bg); color: var(--ink);
17
+ display: flex; flex-direction: column; overflow: hidden;
18
+ }
19
+
20
+ #topbar {
21
+ display: flex; align-items: center; gap: 14px;
22
+ padding: 8px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
23
+ flex: 0 0 auto;
24
+ }
25
+ .brand { font-weight: 700; }
26
+ .brand span { color: var(--accent); }
27
+ .brand em { font-style: normal; font-weight: 400; color: var(--ink2); font-size: 12px; margin-left: 6px; }
28
+ #doc-title { color: var(--ink2); font-size: 13px; }
29
+ .spacer { flex: 1; }
30
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: #d33; }
31
+ .dot.on { background: #2e9e5b; }
32
+
33
+ main { flex: 1; display: flex; min-height: 0; }
34
+
35
+ /* ---- 文档区 ---- */
36
+ #doc-pane { flex: 1; overflow: auto; padding: 20px; min-width: 0; }
37
+ #doc-view {
38
+ background: var(--panel); max-width: 860px; margin: 0 auto;
39
+ padding: 48px 56px; box-shadow: 0 1px 4px rgba(0,0,0,.06);
40
+ border-radius: 6px; min-height: 100%;
41
+ }
42
+ #doc-view.loading { color: var(--ink2); text-align: center; padding-top: 120px; }
43
+ /* docx-preview 产出 */
44
+ .docx-wrapper { background: transparent !important; padding: 0 !important; }
45
+ .docx-wrapper > section.docx {
46
+ box-shadow: none !important; margin: 0 auto 16px !important;
47
+ border: 1px solid var(--line) !important;
48
+ }
49
+ /* tex 渲染 */
50
+ .tex-h1 { font-size: 22px; font-weight: 700; margin: 28px 0 12px; }
51
+ .tex-h2 { font-size: 18px; font-weight: 700; margin: 22px 0 10px; }
52
+ .tex-h3 { font-size: 16px; font-weight: 600; margin: 18px 0 8px; }
53
+ .tex-p { margin: 0 0 10px; text-align: justify; }
54
+
55
+ /* PDF 页(tex 编译产物 / 直接打开的 pdf):canvas + 文本层 */
56
+ .pdf-page { position: relative; margin: 0 auto 16px; border: 1px solid #e4e7ec; border-radius: 4px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
57
+ .pdf-page canvas { display: block; }
58
+ .textLayer { position: absolute; inset: 0; overflow: hidden; opacity: 1; line-height: 1; text-align: initial; text-size-adjust: none; forced-color-adjust: none; }
59
+ .textLayer span { color: transparent; position: absolute; white-space: pre; cursor: text; transform-origin: 0 0; }
60
+ .textLayer ::selection { background: rgba(0, 100, 255, .18); }
61
+ .tex-tip { background: #fff8e6; border: 1px solid #f0d896; border-radius: 6px; padding: 10px 14px; margin-bottom: 16px; font-size: 13px; line-height: 1.6; color: #6b5410; }
62
+ .tex-cap { color: var(--ink2); font-style: italic; border-left: 3px solid var(--line); padding-left: 10px; }
63
+
64
+ /* ---- 高亮 ----
65
+ 荧光笔色号(c0-c4 循环)= 哪张图的上下文;
66
+ 顶部细条 = 审批状态;底部色带 = 重叠进来的其他图(每图一条,越叠越多);
67
+ 重叠区间背景加深纹理(JS 设 inline background/box-shadow) */
68
+ .pf-hl {
69
+ cursor: pointer; border-radius: 2px; padding: 0 1px;
70
+ transition: filter .15s;
71
+ }
72
+ .pf-hl:hover { filter: brightness(.96); }
73
+ .pf-hl.depth2 { background-image: repeating-linear-gradient(45deg, rgba(0,0,0,.05) 0 4px, transparent 4px 9px); }
74
+ .pf-hl.depth3, .pf-hl.depth4 { background-image: repeating-linear-gradient(45deg, rgba(0,0,0,.09) 0 3px, transparent 3px 7px); }
75
+ .pf-hl.flash, .pf-ctx.flash { animation: pf-flash 0.6s ease-in-out 2; }
76
+ @keyframes pf-flash { 0%,100% { filter: none; } 50% { filter: brightness(.82); } }
77
+
78
+ /* 段落级上下文(文本模式):整段淡荧光 + 左侧色带;多图重叠时 title 列出 */
79
+ .pf-ctx {
80
+ cursor: pointer;
81
+ background: linear-gradient(90deg, var(--pf-ctx-bg, #fff3c2), transparent 92%);
82
+ box-shadow: inset 3px 0 0 var(--pf-ctx-line, #e6b800);
83
+ border-radius: 2px;
84
+ }
85
+
86
+ /* ---- 内联图卡片:插在锚点所在块之后 = "图定位到原文的位置" ---- */
87
+ .pf-inline-fig {
88
+ margin: 14px 0; max-width: 460px; border: 1px solid var(--line); border-radius: 8px;
89
+ background: var(--panel, #fff); overflow: hidden; cursor: zoom-in;
90
+ box-shadow: 0 1px 6px rgba(0,0,0,.06);
91
+ }
92
+ .pf-inline-fig:hover { box-shadow: 0 2px 12px rgba(0,0,0,.12); }
93
+ .pf-inline-fig img { display: block; width: 100%; max-height: 340px; object-fit: contain; background: #fafafa; }
94
+ .pf-inline-fig .cap { font-size: 12px; color: var(--ink2); padding: 6px 10px; border-top: 1px solid var(--line); }
95
+
96
+ /* ---- 浮层右栏:锚定原文(这张图取材的文段) ---- */
97
+ .fig-ctx { border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
98
+ .fig-ctx-title { font-size: 12px; font-weight: 600; padding: 7px 10px; background: var(--accent-weak, #eef4ff); color: var(--accent, #3b74d1); }
99
+ .fig-ctx-item { padding: 8px 10px; border-top: 1px solid var(--line); }
100
+ .fig-ctx-item .meta { font-size: 11px; color: var(--ink2); margin-bottom: 4px; }
101
+ .fig-ctx-item .quote { font-size: 13px; line-height: 1.55; color: var(--ink, #222); margin-bottom: 6px; }
102
+
103
+ /* ---- 右栏 ---- */
104
+ #side {
105
+ flex: 0 0 360px; border-left: 1px solid var(--line); background: var(--panel);
106
+ display: flex; flex-direction: column; min-height: 0;
107
+ }
108
+ #tabs { display: flex; border-bottom: 1px solid var(--line); }
109
+ #tabs button {
110
+ flex: 1; padding: 10px 0; border: 0; background: none; cursor: pointer;
111
+ font-size: 13px; color: var(--ink2); border-bottom: 2px solid transparent;
112
+ }
113
+ #tabs button.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
114
+ .cnt { background: var(--accent); color: #fff; border-radius: 8px; font-size: 11px; padding: 0 6px; }
115
+ .tab { display: none; overflow: auto; padding: 12px; flex: 1; }
116
+ .tab.on { display: block; }
117
+ .empty { color: var(--ink2); text-align: center; padding: 32px 0; }
118
+
119
+ .card {
120
+ border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px; overflow: hidden;
121
+ }
122
+ .card img.thumb { width: 100%; display: block; cursor: zoom-in; background: #fafafa; }
123
+ .card .body { padding: 10px 12px; }
124
+ .card .meta { font-size: 12px; color: var(--ink2); margin-bottom: 6px; }
125
+ .badge { font-size: 11px; padding: 1px 8px; border-radius: 8px; margin-right: 6px; }
126
+ .badge.pending { background: var(--yellow); color: #8a6d00; }
127
+ .badge.approved { background: var(--green); color: #1c6b3a; }
128
+ .badge.rejected { background: var(--red); color: #9b2c2c; }
129
+ .badge.changed { background: var(--orange); color: #8a4b0f; }
130
+
131
+ .note-box { width: 100%; border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; font: inherit; resize: vertical; min-height: 40px; }
132
+ .btn-row { display: flex; gap: 8px; margin-top: 8px; }
133
+ .btn { flex: 1; padding: 7px 0; border: 0; border-radius: 6px; cursor: pointer; font-size: 13px; }
134
+ .btn.ok { background: var(--green-line); color: #fff; }
135
+ .btn.ok:hover { filter: brightness(1.08); }
136
+ .btn.no { background: var(--red-line); color: #fff; }
137
+ .btn.no:hover { filter: brightness(1.08); }
138
+ .btn.ghost { background: #eef1f6; color: var(--ink); }
139
+
140
+ .evt { font-size: 12px; padding: 6px 0; border-bottom: 1px dashed var(--line); color: var(--ink2); }
141
+ .evt b { color: var(--ink); font-weight: 600; }
142
+
143
+ /* ---- 浮层 ---- */
144
+ #overlay {
145
+ position: fixed; inset: 0; background: rgba(15,18,25,.72); z-index: 50;
146
+ display: flex; flex-direction: column; padding: 18px;
147
+ }
148
+ #overlay.hidden { display: none; }
149
+ #overlay-head { display: flex; gap: 10px; align-items: center; color: #fff; margin-bottom: 10px; }
150
+ #overlay-head button { border: 0; border-radius: 6px; padding: 6px 14px; cursor: pointer; }
151
+ #overlay-body { flex: 1; overflow: auto; display: flex; gap: 14px; align-items: flex-start; }
152
+ #overlay-body img { max-width: 100%; max-height: 100%; border-radius: 6px; cursor: zoom-in; }
153
+ #overlay-body .ver { background: var(--panel); border-radius: 8px; padding: 10px; max-width: 46%; }
154
+ #overlay-body .ver .meta { font-size: 12px; color: var(--ink2); margin-top: 6px; }
155
+ #overlay.full #overlay-body { align-items: center; justify-content: center; }
156
+ #overlay.full .ver { max-width: 96%; background: none; padding: 0; }
157
+ /* 浮层 = 图(左)+ 就地审批面板(右)—— 审批永远针对点开的那张图 */
158
+ #overlay-main { flex: 1; display: flex; gap: 14px; min-height: 0; }
159
+ #overlay-main #overlay-body { flex: 1; }
160
+ #overlay.full #overlay-main #overlay-body { justify-content: center; }
161
+ #overlay-review {
162
+ flex: 0 0 320px; background: var(--panel); border-radius: 8px; padding: 12px;
163
+ overflow: auto; align-self: stretch;
164
+ }
165
+ #overlay.full #overlay-review { display: none; }
166
+
167
+ #toast {
168
+ position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%);
169
+ background: #1a2233; color: #fff; padding: 9px 18px; border-radius: 8px; z-index: 99;
170
+ }
171
+ #toast.hidden { display: none; }
172
+
173
+ /* ---- 顶栏审批队列入口 ---- */
174
+ #btn-queue {
175
+ border: 1px solid var(--line); background: var(--panel); border-radius: 6px;
176
+ padding: 5px 12px; cursor: pointer; font-size: 13px; color: var(--ink);
177
+ }
178
+ #btn-queue:hover { border-color: var(--accent); color: var(--accent); }
179
+ #btn-queue .cnt { margin-left: 2px; }
180
+ #btn-queue .cnt:empty { display: none; }
181
+
182
+ /* ---- 审批队列浮层:大图 + 就地审批,逐张过 ---- */
183
+ #queue {
184
+ position: fixed; inset: 0; background: rgba(15,18,25,.78); z-index: 60;
185
+ display: flex; flex-direction: column; padding: 16px;
186
+ }
187
+ #queue.hidden { display: none; }
188
+ #queue-head { display: flex; gap: 12px; align-items: center; color: #fff; margin-bottom: 10px; }
189
+ #queue-head button { border: 0; border-radius: 6px; padding: 6px 14px; cursor: pointer; }
190
+ #queue-pos { color: rgba(255,255,255,.75); font-size: 13px; }
191
+ #queue-main { flex: 1; display: flex; gap: 14px; min-height: 0; }
192
+ #queue-img {
193
+ flex: 1; display: flex; align-items: center; justify-content: center; min-width: 0;
194
+ background: rgba(255,255,255,.04); border-radius: 8px; padding: 10px;
195
+ }
196
+ #queue-img img { max-width: 100%; max-height: 100%; border-radius: 6px; cursor: zoom-in; background: #fff; }
197
+ #queue-side {
198
+ flex: 0 0 340px; background: var(--panel); border-radius: 8px; padding: 14px;
199
+ overflow: auto; align-self: stretch;
200
+ }
201
+
202
+ /* ---- 内联图:已驳回 → 占位条(不挂图,文章保持干净) ---- */
203
+ .pf-inline-fig.rejected-strip {
204
+ max-width: 560px; cursor: pointer;
205
+ background: var(--red); border-color: var(--red-line);
206
+ }
207
+ .pf-inline-fig.rejected-strip .cap { color: #7a1f1f; border-top: 0; }
208
+ .pf-inline-fig .cap .link { color: var(--accent); text-decoration: underline; cursor: pointer; }