paperdepot 0.1.1 → 0.1.3
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.en.md +14 -5
- package/README.md +35 -22
- package/lib/client.js +675 -179
- package/lib/impl.js +136 -11
- package/package.json +1 -1
- package/pipeline/config/tagmap_/344/273/252/345/231/250.json +554 -554
- package/pipeline/config/tagmap_/345/272/224/347/224/250.json +626 -626
- package/pipeline/config/tagmap_/346/226/271/346/263/225.json +890 -890
- package/pipeline/config/tagmap_/351/242/206/345/237/237.json +221 -221
- package/pipeline/tools/_paths.py +49 -8
- package/pipeline/tools/mineru_parse.py +537 -462
package/lib/client.js
CHANGED
|
@@ -41,6 +41,41 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
const SP = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 }
|
|
42
42
|
const R = { sm: 6, md: 10, lg: 14 }
|
|
43
43
|
|
|
44
|
+
// 分类语义色:四类受控标签固定色,概念页单独一色。
|
|
45
|
+
// 与 DOMAIN_COLORS(按领域名轮转的识别色)分工:分类色用于 chip / 标签,领域色用于列表里的领域点。
|
|
46
|
+
const CAT_COLOR = { 领域: '#6ea8fe', 方法: '#4ec9a5', 应用: '#e6b450', 仪器: '#c792ea', 状态: '#8b90a0' }
|
|
47
|
+
const CONCEPT_COLOR = '#ff8fab'
|
|
48
|
+
const DOMAIN_COLORS = ['#60a5fa', '#34d399', '#fbbf24', '#a78bfa', '#38bdf8', '#fb923c', '#4ade80', '#e879f9', '#22d3ee', '#f87171', '#a3e635', '#f472b6']
|
|
49
|
+
|
|
50
|
+
function hexA(hex, a) {
|
|
51
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(String(hex || ''))
|
|
52
|
+
if (!m) return hex
|
|
53
|
+
const n = parseInt(m[1], 16)
|
|
54
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tagCategory(tag) {
|
|
58
|
+
const s = String(tag || '')
|
|
59
|
+
const i = s.indexOf('/')
|
|
60
|
+
if (i > 0) return s.slice(0, i)
|
|
61
|
+
const j = s.indexOf('-')
|
|
62
|
+
return j > 0 ? s.slice(0, j) : ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tagColor(tag) {
|
|
66
|
+
return CAT_COLOR[tagCategory(tag)] || T.text3
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function domainOfPaper(p) {
|
|
70
|
+
const t = (p.tags || []).find((x) => x.startsWith('领域/'))
|
|
71
|
+
return t ? t.slice(3) : ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function domainColor(domain, domains) {
|
|
75
|
+
const i = domains.indexOf(domain)
|
|
76
|
+
return DOMAIN_COLORS[(i < 0 ? 0 : i) % DOMAIN_COLORS.length]
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
const fmt = (n) => (typeof n === 'number' ? n.toLocaleString('zh-CN') : '—')
|
|
45
80
|
const api = (p) => fetch('/paperdepot/api' + p, { credentials: 'same-origin' })
|
|
46
81
|
|
|
@@ -126,7 +161,7 @@ window.__ModuleLoader__.load({
|
|
|
126
161
|
{ key: x.key, style: { display: 'flex', alignItems: 'center', gap: 10, fontSize: FS.caption } },
|
|
127
162
|
h('div', { style: { width: 132, textAlign: 'right', color: muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: '0 0 auto' }, title: x.key }, x.label),
|
|
128
163
|
h('div', { style: { flex: '1 1 auto', height: 10, background: 'rgba(127,127,127,0.10)', borderRadius: 999, overflow: 'hidden' } },
|
|
129
|
-
h('div', { style: { width: `${Math.round((x.value / max) * 100)}%`, height: '100%', background: color, borderRadius: 999 } })),
|
|
164
|
+
h('div', { style: { width: `${Math.round((x.value / max) * 100)}%`, height: '100%', background: x.color || color, borderRadius: 999 } })),
|
|
130
165
|
h('div', { style: { width: 44, color: faint, flex: '0 0 auto', textAlign: 'right' } }, x.value),
|
|
131
166
|
),
|
|
132
167
|
),
|
|
@@ -149,6 +184,371 @@ window.__ModuleLoader__.load({
|
|
|
149
184
|
return h('div', { style: { padding: '28px 16px', textAlign: 'center', color: faint, fontSize: FS.body, border: `1px dashed ${line}`, borderRadius: R.lg } }, children)
|
|
150
185
|
}
|
|
151
186
|
|
|
187
|
+
// ---------------- 条目排版(论文 index.md / 概念页共用,无外部依赖) ----------------
|
|
188
|
+
// 面板里直接塞 <pre> 会把 YAML、导航块、表格原文全部倒出来;这里做一层轻量渲染:
|
|
189
|
+
// 去 frontmatter / 导航注释 / 由面板另行呈现的尾部小节,再按标题、列表、表格、引用排版。
|
|
190
|
+
|
|
191
|
+
const MD_FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/
|
|
192
|
+
const MD_HIDDEN_SECTIONS = ['标签', '相关文件', '全文', '原始文件', '所属概念', '分类', '相关论文']
|
|
193
|
+
const MD_EMOJI_HEAD = /^[^\p{L}\p{N}\u4e00-\u9fff]+/u
|
|
194
|
+
const MD_BLOCK_START = /^(#{1,6}\s|>\s?|\s*(?:[-*+]|\d+\.)\s+|\||!\[)/
|
|
195
|
+
|
|
196
|
+
const LATEX_MAP = [
|
|
197
|
+
[/\\times/g, '×'], [/\\cdot/g, '·'], [/\\pm/g, '±'], [/\\approx/g, '≈'], [/\\sim/g, '~'],
|
|
198
|
+
[/\\leq?/g, '≤'], [/\\geq?/g, '≥'], [/\\neq?/g, '≠'], [/\\to/g, '→'], [/\\infty/g, '∞'],
|
|
199
|
+
[/\\theta/g, 'θ'], [/\\phi/g, 'φ'], [/\\varphi/g, 'φ'], [/\\alpha/g, 'α'], [/\\beta/g, 'β'],
|
|
200
|
+
[/\\gamma/g, 'γ'], [/\\delta/g, 'δ'], [/\\Delta/g, 'Δ'], [/\\lambda/g, 'λ'], [/\\mu/g, 'μ'],
|
|
201
|
+
[/\\sigma/g, 'σ'], [/\\Sigma/g, 'Σ'], [/\\omega/g, 'ω'], [/\\Omega/g, 'Ω'], [/\\tau/g, 'τ'],
|
|
202
|
+
[/\\rho/g, 'ρ'], [/\\nu/g, 'ν'], [/\\epsilon/g, 'ε'], [/\\psi/g, 'ψ'], [/\\circ/g, '°'],
|
|
203
|
+
[/\\tan/g, 'tan'], [/\\log/g, 'log'], [/\\exp/g, 'exp'], [/\\sin/g, 'sin'], [/\\cos/g, 'cos'],
|
|
204
|
+
[/\\mathrm\{([^}]*)\}/g, '$1'], [/\\text\{([^}]*)\}/g, '$1'],
|
|
205
|
+
[/\\left[([|.]/g, '$1'], [/\\right[)\]|.]/g, '$1'],
|
|
206
|
+
[/\\[,;: ]/g, ' '], [/\\!/g, ''], [/\\%/g, '%'], [/\\_/g, '_'], [/\\/g, ''],
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
const LATEX_SUP = { 0: '⁰', 1: '¹', 2: '²', 3: '³', 4: '⁴', 5: '⁵', 6: '⁶', 7: '⁷', 8: '⁸', 9: '⁹', '-': '⁻', '+': '⁺' }
|
|
210
|
+
const LATEX_SUB = { 0: '₀', 1: '₁', 2: '₂', 3: '₃', 4: '₄', 5: '₅', 6: '₆', 7: '₇', 8: '₈', 9: '₉', '-': '₋' }
|
|
211
|
+
const mapsTo = (m) => [...m].map((x) => LATEX_SUP[x] || x).join('')
|
|
212
|
+
const mapsToSub = (m) => [...m].map((x) => LATEX_SUB[x] || x).join('')
|
|
213
|
+
|
|
214
|
+
// 正文里也常出现未包 $ 的 10^{-6}、s^{-1},顺手换成上/下标,避免花括号漏出来
|
|
215
|
+
function prettyPlain(s) {
|
|
216
|
+
return String(s || '')
|
|
217
|
+
.replace(/\^\{([-+0-9]+)\}/g, (m, g) => mapsTo(g))
|
|
218
|
+
.replace(/\^(-?[0-9]+)/g, (m, g) => mapsTo(g))
|
|
219
|
+
.replace(/_\{([0-9]+)\}/g, (m, g) => mapsToSub(g))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function prettyMath(s) {
|
|
223
|
+
let out = String(s || '')
|
|
224
|
+
for (const [re, rep] of LATEX_MAP) out = out.replace(re, rep)
|
|
225
|
+
out = out
|
|
226
|
+
.replace(/\^\{([-+0-9]+)\}/g, (m, g) => mapsTo(g))
|
|
227
|
+
.replace(/\^(-?[0-9]+)/g, (m, g) => mapsTo(g))
|
|
228
|
+
.replace(/_\{([0-9]+)\}/g, (m, g) => mapsToSub(g))
|
|
229
|
+
.replace(/_([0-9]+)/g, (m, g) => mapsToSub(g))
|
|
230
|
+
.replace(/[{}]/g, '')
|
|
231
|
+
.replace(/([µμ])\s+(?=[a-zA-Z])/g, '$1')
|
|
232
|
+
.replace(/[ \t]+/g, ' ')
|
|
233
|
+
.trim()
|
|
234
|
+
return out
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parsePaperFrontmatter(md) {
|
|
238
|
+
const m = MD_FRONTMATTER.exec(String(md || ''))
|
|
239
|
+
const block = m ? m[1] : ''
|
|
240
|
+
const scalar = (k) => {
|
|
241
|
+
const r = new RegExp('^' + k + ':\\s*(.+?)\\s*$', 'm').exec(block)
|
|
242
|
+
return r ? r[1].trim().replace(/^["']|["']$/g, '') : ''
|
|
243
|
+
}
|
|
244
|
+
const list = (k) => {
|
|
245
|
+
const r = new RegExp('^' + k + ':\\s*\\n((?:[ \\t]*-[^\\n]*\\n?)+)', 'm').exec(block)
|
|
246
|
+
if (!r) return []
|
|
247
|
+
return r[1]
|
|
248
|
+
.split(/\r?\n/)
|
|
249
|
+
.map((x) => x.trim().replace(/^-\s*/, '').replace(/^["']|["']$/g, ''))
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
}
|
|
252
|
+
const fm = {}
|
|
253
|
+
for (const k of ['title', 'year', 'journal', 'doi', 'arxiv', 'pages', 'figures', 'status', 'citekey', 'updated']) fm[k] = scalar(k)
|
|
254
|
+
fm.tags = list('tags')
|
|
255
|
+
fm.aliases = list('aliases')
|
|
256
|
+
return fm
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stripEntryChrome(md) {
|
|
260
|
+
let s = String(md || '')
|
|
261
|
+
s = s.replace(/<!--\s*[\w-]+:nav:start\s*-->[\s\S]*?<!--\s*[\w-]+:nav:end\s*-->/g, '')
|
|
262
|
+
s = s.replace(/<!--[\s\S]*?-->/g, '')
|
|
263
|
+
s = s.replace(MD_FRONTMATTER, '')
|
|
264
|
+
const out = []
|
|
265
|
+
let skip = false
|
|
266
|
+
for (const line of s.split(/\r?\n/)) {
|
|
267
|
+
const m = /^##\s+(.+?)\s*$/.exec(line)
|
|
268
|
+
if (m) skip = MD_HIDDEN_SECTIONS.some((n) => m[1].includes(n))
|
|
269
|
+
if (!skip) out.push(line)
|
|
270
|
+
}
|
|
271
|
+
return out.join('\n')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function splitTableRow(line) {
|
|
275
|
+
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderInline(text, keyBase, opts) {
|
|
279
|
+
const s = String(text == null ? '' : text)
|
|
280
|
+
const re = /(\[\[[^\]\n]+\]\]|\*\*[^*\n]+\*\*|`[^`\n]+`|\$[^$\n]+\$|\[[^\]\n]+\]\([^)\n]+\)|\*[^*\n]+\*)/g
|
|
281
|
+
const out = []
|
|
282
|
+
let last = 0
|
|
283
|
+
let n = 0
|
|
284
|
+
let m
|
|
285
|
+
while ((m = re.exec(s))) {
|
|
286
|
+
if (m.index > last) out.push(prettyPlain(s.slice(last, m.index)))
|
|
287
|
+
const tok = m[0]
|
|
288
|
+
const key = keyBase + '-' + n++
|
|
289
|
+
if (tok.startsWith('[[')) {
|
|
290
|
+
const inner = tok.slice(2, -2)
|
|
291
|
+
const cut = inner.indexOf('|')
|
|
292
|
+
const target = (cut === -1 ? inner : inner.slice(0, cut)).trim()
|
|
293
|
+
const alias = (cut === -1 ? inner : inner.slice(cut + 1)).trim()
|
|
294
|
+
const open = opts && opts.onOpenPaper && /^1-Papers\//.test(target)
|
|
295
|
+
if (target.startsWith('2-Concepts/')) {
|
|
296
|
+
out.push(h(ConceptChip, { key, name: alias || target.slice('2-Concepts/'.length) }))
|
|
297
|
+
} else if (target.startsWith('3-Taxonomy/')) {
|
|
298
|
+
out.push(h(TaxChip, { key, id: (target.split('/')[1] || '').replace(/\.md$/, '') }))
|
|
299
|
+
} else {
|
|
300
|
+
out.push(
|
|
301
|
+
h('span', {
|
|
302
|
+
key,
|
|
303
|
+
title: target,
|
|
304
|
+
onClick: open ? () => opts.onOpenPaper(target.replace(/^1-Papers\//, '').replace(/\/index.*$/, '')) : undefined,
|
|
305
|
+
style: { color: T.text2, borderBottom: `1px dotted ${T.b2}`, cursor: open ? 'pointer' : 'default' },
|
|
306
|
+
}, alias || target),
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
} else if (tok.startsWith('**')) {
|
|
310
|
+
out.push(h('strong', { key, style: { fontWeight: 600, color: T.text } }, tok.slice(2, -2)))
|
|
311
|
+
} else if (tok.startsWith('`')) {
|
|
312
|
+
out.push(h('code', { key, style: { fontFamily: mono, fontSize: '0.92em', background: 'rgba(255,255,255,0.06)', padding: '1px 5px', borderRadius: 4 } }, tok.slice(1, -1)))
|
|
313
|
+
} else if (tok.startsWith('$')) {
|
|
314
|
+
out.push(h('span', { key, style: { fontFamily: mono, fontSize: '0.92em', color: '#c8c9d4' } }, prettyMath(tok.slice(1, -1))))
|
|
315
|
+
} else if (tok.startsWith('[')) {
|
|
316
|
+
const mm = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(tok)
|
|
317
|
+
out.push(h('a', { key, href: mm[2], target: '_blank', rel: 'noreferrer', style: { color: '#8fa3ff', textDecoration: 'none' } }, mm[1]))
|
|
318
|
+
} else {
|
|
319
|
+
out.push(h('em', { key, style: { fontStyle: 'normal', color: T.text2 } }, tok.slice(1, -1)))
|
|
320
|
+
}
|
|
321
|
+
last = m.index + tok.length
|
|
322
|
+
}
|
|
323
|
+
if (last < s.length) out.push(prettyPlain(s.slice(last)))
|
|
324
|
+
return out.length ? out : ['']
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function mdTable(header, rows, keyBase) {
|
|
328
|
+
const cell = (text, i) => {
|
|
329
|
+
const isNum = /^[\d.,%×+\-–—\s/]+$/.test(text) && /\d/.test(text)
|
|
330
|
+
return h('td', {
|
|
331
|
+
key: keyBase + '-c' + i,
|
|
332
|
+
style: {
|
|
333
|
+
padding: '8px 10px', borderBottom: '1px solid rgba(255,255,255,0.05)', lineHeight: 1.6, verticalAlign: 'top',
|
|
334
|
+
fontFamily: isNum ? mono : 'inherit', fontSize: isNum ? 12.5 : 13,
|
|
335
|
+
fontVariantNumeric: isNum ? 'tabular-nums' : undefined, color: isNum ? T.text2 : 'inherit',
|
|
336
|
+
},
|
|
337
|
+
}, renderInline(text, keyBase + '-c' + i))
|
|
338
|
+
}
|
|
339
|
+
return h('table', { key: keyBase, style: { width: '100%', borderCollapse: 'collapse', margin: '10px 0 18px', fontSize: 13 } },
|
|
340
|
+
h('thead', null, h('tr', null, header.map((c, i) => h('th', {
|
|
341
|
+
key: keyBase + '-h' + i,
|
|
342
|
+
style: { textAlign: 'left', fontSize: 12, fontWeight: 600, color: T.text3, padding: '7px 10px', borderBottom: '1px solid rgba(255,255,255,0.1)', whiteSpace: 'nowrap' },
|
|
343
|
+
}, renderInline(c, keyBase + '-h' + i))))),
|
|
344
|
+
h('tbody', null, rows.map((cells, r) => h('tr', { key: keyBase + '-r' + r }, cells.map(cell)))))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function mdList(items, ordered, keyBase) {
|
|
348
|
+
return h('div', { key: keyBase, style: { margin: '0 0 14px' } },
|
|
349
|
+
items.map((it, i) => h('div', { key: keyBase + '-' + i, style: { display: 'flex', gap: 8, marginBottom: 5 } },
|
|
350
|
+
h('span', {
|
|
351
|
+
style: {
|
|
352
|
+
flex: '0 0 auto', minWidth: ordered ? 17 : 10, color: T.caption, lineHeight: 1.8,
|
|
353
|
+
fontFamily: ordered ? mono : 'inherit', fontSize: ordered ? 12 : 13,
|
|
354
|
+
},
|
|
355
|
+
}, ordered ? (i + 1) + '.' : '–'),
|
|
356
|
+
h('span', { style: { flex: '1 1 auto', lineHeight: 1.8 } }, renderInline(it, keyBase + '-' + i)))))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function mdLede(text, keyBase) {
|
|
360
|
+
const clean = String(text || '').replace(/^(\*\*)?一句话(\*\*)?\s*[::]\s*/, '')
|
|
361
|
+
return h('div', {
|
|
362
|
+
key: keyBase,
|
|
363
|
+
style: { margin: '0 0 26px', padding: '2px 0 2px 18px', borderLeft: `2px solid ${ACCENT}`, fontSize: 16.5, lineHeight: 1.72, color: T.text },
|
|
364
|
+
}, renderInline(clean, keyBase, null))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function mdQuote(text, keyBase) {
|
|
368
|
+
const clean = String(text || '').replace(/^(\*\*)?一句话(\*\*)?\s*[::]\s*/, '')
|
|
369
|
+
return h('div', {
|
|
370
|
+
key: keyBase,
|
|
371
|
+
style: { margin: '0 0 14px', padding: '2px 0 2px 14px', borderLeft: `2px solid ${T.b2}`, color: T.text2, lineHeight: 1.8 },
|
|
372
|
+
}, renderInline(clean, keyBase, null))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function Markdown({ source, ledeHeading, ledeFirstQuote, onOpenPaper }) {
|
|
376
|
+
const src = stripEntryChrome(source)
|
|
377
|
+
const lines = src.split(/\r?\n/)
|
|
378
|
+
const blocks = []
|
|
379
|
+
const opts = { onOpenPaper }
|
|
380
|
+
let key = 0
|
|
381
|
+
let ledeDone = false
|
|
382
|
+
let sawSection = false
|
|
383
|
+
const k = () => 'md' + key++
|
|
384
|
+
let i = 0
|
|
385
|
+
while (i < lines.length) {
|
|
386
|
+
const line = lines[i]
|
|
387
|
+
if (!line.trim()) {
|
|
388
|
+
i += 1
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
if (line.trim().startsWith('|') && i + 1 < lines.length && /^\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1].trim())) {
|
|
392
|
+
// 条目开头的基础信息表(页数/图数/作者…)与面板的元数据条重复,跳过
|
|
393
|
+
if (!sawSection) {
|
|
394
|
+
i += 2
|
|
395
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) i += 1
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
const header = splitTableRow(line)
|
|
399
|
+
const rows = []
|
|
400
|
+
i += 2
|
|
401
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) {
|
|
402
|
+
rows.push(splitTableRow(lines[i]))
|
|
403
|
+
i += 1
|
|
404
|
+
}
|
|
405
|
+
blocks.push(mdTable(header, rows, k()))
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
const hm = /^(#{1,6})\s+(.+?)\s*$/.exec(line)
|
|
409
|
+
if (hm) {
|
|
410
|
+
const level = hm[1].length
|
|
411
|
+
const text = hm[2].replace(MD_EMOJI_HEAD, '').trim()
|
|
412
|
+
i += 1
|
|
413
|
+
if (level === 1) continue
|
|
414
|
+
if (level === 2) sawSection = true
|
|
415
|
+
if (level === 2 && ledeHeading && text.includes(ledeHeading) && !ledeDone) {
|
|
416
|
+
const quote = []
|
|
417
|
+
while (i < lines.length && (!lines[i].trim() || /^>\s?/.test(lines[i]))) {
|
|
418
|
+
if (lines[i].trim()) quote.push(lines[i].replace(/^>\s?/, ''))
|
|
419
|
+
i += 1
|
|
420
|
+
}
|
|
421
|
+
blocks.push(mdLede(quote.join(' '), k()))
|
|
422
|
+
ledeDone = true
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
if (level === 2) {
|
|
426
|
+
blocks.push(h('div', {
|
|
427
|
+
key: k(),
|
|
428
|
+
style: { margin: '30px 0 10px', paddingTop: 14, borderTop: '1px solid rgba(255,255,255,0.06)', fontSize: FS.sub, fontWeight: 600, color: T.text },
|
|
429
|
+
}, text))
|
|
430
|
+
} else {
|
|
431
|
+
blocks.push(h('div', { key: k(), style: { margin: '20px 0 6px', fontSize: 13.5, fontWeight: 600, color: T.text2 } }, text))
|
|
432
|
+
}
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
if (/^>\s?/.test(line)) {
|
|
436
|
+
const quote = []
|
|
437
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
438
|
+
quote.push(lines[i].replace(/^>\s?/, ''))
|
|
439
|
+
i += 1
|
|
440
|
+
}
|
|
441
|
+
if (ledeFirstQuote && !ledeDone) {
|
|
442
|
+
blocks.push(mdLede(quote.join(' '), k()))
|
|
443
|
+
ledeDone = true
|
|
444
|
+
} else {
|
|
445
|
+
blocks.push(mdQuote(quote.join(' '), k()))
|
|
446
|
+
}
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
|
|
450
|
+
const ordered = /^\s*\d+\./.test(line)
|
|
451
|
+
const items = []
|
|
452
|
+
while (i < lines.length && /^\s*(?:[-*+]|\d+\.)\s+/.test(lines[i])) {
|
|
453
|
+
items.push(lines[i].replace(/^\s*(?:[-*+]|\d+\.)\s+/, ''))
|
|
454
|
+
i += 1
|
|
455
|
+
while (i < lines.length && /^\s{2,}\S/.test(lines[i]) && !/^\s*(?:[-*+]|\d+\.)\s+/.test(lines[i])) {
|
|
456
|
+
items[items.length - 1] += ' ' + lines[i].trim()
|
|
457
|
+
i += 1
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
blocks.push(mdList(items, ordered, k()))
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
const img = /^!\[([^\]]*)\]\(([^)]+)\)\s*$/.exec(line.trim())
|
|
464
|
+
if (img) {
|
|
465
|
+
blocks.push(h('div', { key: k(), style: { margin: '6px 0', fontSize: 12.5, color: T.caption } }, img[1] ? '图:' + img[1] : '[图]'))
|
|
466
|
+
i += 1
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
|
|
470
|
+
blocks.push(h('div', { key: k(), style: { height: 1, background: 'rgba(255,255,255,0.06)', margin: '22px 0' } }))
|
|
471
|
+
i += 1
|
|
472
|
+
continue
|
|
473
|
+
}
|
|
474
|
+
const para = [line]
|
|
475
|
+
i += 1
|
|
476
|
+
while (i < lines.length && lines[i].trim() && !MD_BLOCK_START.test(lines[i])) {
|
|
477
|
+
para.push(lines[i])
|
|
478
|
+
i += 1
|
|
479
|
+
}
|
|
480
|
+
blocks.push(h('p', { key: k(), style: { margin: '0 0 12px', lineHeight: 1.85 } }, renderInline(para.join(' '), k(), opts)))
|
|
481
|
+
}
|
|
482
|
+
return h('div', { style: { color: '#dcdce2' } }, blocks)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------- 阅读区共用小组件 ----------------
|
|
486
|
+
|
|
487
|
+
function MetaStrip({ items }) {
|
|
488
|
+
const shown = (items || []).filter((x) => x && x.value)
|
|
489
|
+
if (!shown.length) return null
|
|
490
|
+
return h('div', { style: { marginTop: 16, display: 'flex', flexWrap: 'wrap', gap: '14px 30px' } },
|
|
491
|
+
shown.map((x) => h('div', { key: x.label, style: { display: 'flex', flexDirection: 'column', gap: 3 } },
|
|
492
|
+
h('div', { style: { fontSize: 11.5, color: T.caption } }, x.label),
|
|
493
|
+
h('div', { style: { fontFamily: mono, fontSize: 12.5, color: T.text2, fontVariantNumeric: 'tabular-nums' } }, x.value))))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function ConceptChip({ name, title }) {
|
|
497
|
+
return h('span', {
|
|
498
|
+
title: title || '概念 · ' + name,
|
|
499
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 6, height: 25, padding: '0 9px', borderRadius: 6, background: hexA(CONCEPT_COLOR, 0.13), color: CONCEPT_COLOR, fontSize: 12, whiteSpace: 'nowrap' },
|
|
500
|
+
}, h('span', { style: { width: 5, height: 5, borderRadius: '50%', background: CONCEPT_COLOR } }), name)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function TaxChip({ id }) {
|
|
504
|
+
const color = tagColor(id)
|
|
505
|
+
const label = String(id).replace(/^([^/-]+)[/-]/, '$1 · ')
|
|
506
|
+
return h('span', {
|
|
507
|
+
title: String(id),
|
|
508
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 6, height: 25, padding: '0 9px', borderRadius: 6, background: hexA(color, 0.13), color, fontSize: 12, whiteSpace: 'nowrap' },
|
|
509
|
+
}, h('span', { style: { width: 5, height: 5, borderRadius: '50%', background: color } }), label)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function RailHead({ title, count }) {
|
|
513
|
+
return h('div', { style: { padding: '16px 16px 10px', display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' } },
|
|
514
|
+
h('div', { style: { fontSize: 13, fontWeight: 600, color: T.text2 } }, title),
|
|
515
|
+
h('span', { style: { fontFamily: mono, fontSize: 12, color: T.caption, fontVariantNumeric: 'tabular-nums' } }, count))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function RailSearch({ value, onChange, placeholder }) {
|
|
519
|
+
return h('div', {
|
|
520
|
+
style: { margin: '0 12px 8px', height: 36, display: 'flex', alignItems: 'center', gap: 8, padding: '0 11px', background: T.bg2, border: '1px solid transparent', borderRadius: 8 },
|
|
521
|
+
},
|
|
522
|
+
h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', style: { flex: '0 0 auto', opacity: 0.55 } },
|
|
523
|
+
h('circle', { cx: 7, cy: 7, r: 4.6, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
524
|
+
h('path', { d: 'M10.5 10.5 14 14', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round' })),
|
|
525
|
+
h('input', {
|
|
526
|
+
value,
|
|
527
|
+
onChange: (e) => onChange(e.target.value),
|
|
528
|
+
placeholder,
|
|
529
|
+
style: { flex: '1 1 auto', minWidth: 0, border: 'none', background: 'transparent', color: T.text, fontSize: 13.5, fontFamily: 'inherit', caretColor: ACCENT },
|
|
530
|
+
}))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function ReaderEmpty({ title, sub }) {
|
|
534
|
+
return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, padding: 24 } },
|
|
535
|
+
h('div', { style: { fontSize: 14.5, fontWeight: 600, color: T.text2 } }, title),
|
|
536
|
+
sub ? h('div', { style: { fontSize: 12.5, color: T.caption, textAlign: 'center' } }, sub) : null)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function ReaderPane({ children }) {
|
|
540
|
+
return h('div', { style: { minHeight: 0, overflowY: 'auto' } },
|
|
541
|
+
h('div', { style: { maxWidth: 760, padding: '30px 32px 72px', boxSizing: 'border-box' } }, children))
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function DocActions({ children }) {
|
|
545
|
+
return h('div', { style: { flex: '0 0 auto', display: 'flex', gap: 6, marginTop: 2 } }, children)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function H2({ children }) {
|
|
549
|
+
return h('div', { style: { margin: '30px 0 10px', paddingTop: 14, borderTop: '1px solid rgba(255,255,255,0.06)', fontSize: FS.sub, fontWeight: 600, color: T.text } }, children)
|
|
550
|
+
}
|
|
551
|
+
|
|
152
552
|
function SetupCard({ setup, onSaved }) {
|
|
153
553
|
const [key, setKey] = useState('')
|
|
154
554
|
const [vault, setVault] = useState(setup.vault || '')
|
|
@@ -301,6 +701,18 @@ window.__ModuleLoader__.load({
|
|
|
301
701
|
`Python:${setup.python && setup.python.ok ? '✅ ' + (setup.python.version || '可用') : '❌ 未找到(' + ((setup.python && setup.python.exe) || 'python') + ')'}`,
|
|
302
702
|
h('br'),
|
|
303
703
|
`MinerU key:${setup.keySource === 'env' ? '来自环境变量 MINERU_API_KEY' : setup.hasKey ? '已保存(…' + setup.keyTail + ')' : '未配置'}`,
|
|
704
|
+
setup.hasKey && setup.pythonKey
|
|
705
|
+
? h(
|
|
706
|
+
'div',
|
|
707
|
+
{ style: { marginTop: 6, color: setup.pythonKey.ok === false ? ERROR_TEXT : muted, fontWeight: setup.pythonKey.ok === false ? 600 : 400 } },
|
|
708
|
+
setup.pythonKey.ok === true
|
|
709
|
+
? '✓ Python 流水线能读到同一个 key'
|
|
710
|
+
: setup.pythonKey.ok === false
|
|
711
|
+
? '⚠ Python 流水线读不到这个 key,导入会失败:' + (setup.pythonKey.detail || '未知原因') +
|
|
712
|
+
'。请把插件升级到含修复的版本,或给 DSH 进程设置环境变量 MINERU_API_KEY 后重启。'
|
|
713
|
+
: '(Python key 自检已跳过:' + (setup.pythonKey.detail || '流水线/解释器不可用') + ')',
|
|
714
|
+
)
|
|
715
|
+
: null,
|
|
304
716
|
h('br'),
|
|
305
717
|
`密钥文件:${setup.secretsPath}`,
|
|
306
718
|
),
|
|
@@ -519,14 +931,14 @@ window.__ModuleLoader__.load({
|
|
|
519
931
|
}
|
|
520
932
|
|
|
521
933
|
function Overview({ d, onOpen }) {
|
|
522
|
-
const tagItems = (d.tags || []).slice(0, 14).map((t) => ({ key: t.tag, label: t.tag, value: t.count }))
|
|
934
|
+
const tagItems = (d.tags || []).slice(0, 14).map((t) => ({ key: t.tag, label: t.tag, value: t.count, color: tagColor(t.tag) }))
|
|
523
935
|
const yearItems = (d.years || []).slice(-14).map((y) => ({ key: String(y.year), label: String(y.year), value: y.count }))
|
|
524
936
|
return h(
|
|
525
937
|
'div',
|
|
526
938
|
null,
|
|
527
939
|
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: SP.md } },
|
|
528
940
|
h(Card, { label: '论文条目', value: fmt(d.papers) }),
|
|
529
|
-
h(Card, { label: '概念页', value: fmt(d.concepts) }),
|
|
941
|
+
h(Card, { label: '概念页', value: fmt(d.concepts), accent: d.concepts ? CONCEPT_COLOR : undefined }),
|
|
530
942
|
h(Card, { label: '待补元数据', value: fmt(d.pendingMeta), accent: d.pendingMeta ? WARN_TEXT : undefined }),
|
|
531
943
|
h(Card, { label: '待生成摘要', value: fmt(d.pendingSummary), accent: d.pendingSummary ? WARN_TEXT : undefined, sub: d.pendingSummary ? '导入后未跑摘要' : '全部齐全' }),
|
|
532
944
|
),
|
|
@@ -557,15 +969,46 @@ window.__ModuleLoader__.load({
|
|
|
557
969
|
(d.conceptNames || []).length
|
|
558
970
|
? h(Section, { title: `概念层(${d.concepts} 页)` },
|
|
559
971
|
h('div', { style: { display: 'flex', gap: SP.sm, flexWrap: 'wrap' } },
|
|
560
|
-
d.conceptNames.map((n) => h(
|
|
972
|
+
d.conceptNames.map((n) => h(ConceptChip, { key: n, name: n }))))
|
|
561
973
|
: null,
|
|
562
974
|
)
|
|
563
975
|
}
|
|
564
976
|
|
|
977
|
+
// 批量补摘要进度条:贴在内容区底部,任务结束后保留可收起
|
|
978
|
+
function BatchBar({ job, onCancel, onClose }) {
|
|
979
|
+
const failed = (job.failed || []).length
|
|
980
|
+
const ok = Math.max(0, (job.done || 0) - failed)
|
|
981
|
+
const label = job.state === 'running'
|
|
982
|
+
? `批量补摘要:${job.done || 0}/${job.total || 0}${failed ? `,失败 ${failed}` : ''}`
|
|
983
|
+
: `批量补摘要结束:成功 ${ok}/${job.total || 0}${failed ? `,失败 ${failed}` : ''}${job.state === 'cancelled' ? '(已停止,再点继续)' : ''}`
|
|
984
|
+
return h(
|
|
985
|
+
'div',
|
|
986
|
+
{
|
|
987
|
+
style: {
|
|
988
|
+
position: 'absolute', left: 20, right: 20, bottom: 24, zIndex: 30,
|
|
989
|
+
background: T.bg1, border: `1px solid ${line}`, borderRadius: R.md,
|
|
990
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.28)', padding: '10px 12px',
|
|
991
|
+
display: 'flex', flexDirection: 'column', gap: 6,
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: SP.md } },
|
|
995
|
+
h('div', { style: { flex: '1 1 auto', fontSize: FS.body, fontWeight: 600 } }, label),
|
|
996
|
+
job.state === 'running'
|
|
997
|
+
? h(Btn, { small: true, ghost: true, onClick: onCancel, title: '跑完当前这篇后停下,下次再点继续' }, '停止')
|
|
998
|
+
: h(Btn, { small: true, ghost: true, onClick: onClose }, '收起')),
|
|
999
|
+
h('div', {
|
|
1000
|
+
style: {
|
|
1001
|
+
fontFamily: mono, fontSize: 11.5, color: muted, lineHeight: 1.5,
|
|
1002
|
+
maxHeight: 88, overflowY: 'auto', whiteSpace: 'pre-wrap',
|
|
1003
|
+
},
|
|
1004
|
+
}, (job.lines || []).slice(-5).join('\n')))
|
|
1005
|
+
}
|
|
1006
|
+
|
|
565
1007
|
function Papers({ papers, loading, onSelect, selected, selectedData, onSummarize, running, vaultName }) {
|
|
566
1008
|
const [q, setQ] = useState('')
|
|
567
1009
|
const [domain, setDomain] = useState('')
|
|
568
1010
|
const [onlyPending, setOnlyPending] = useState(false)
|
|
1011
|
+
const [hovered, setHovered] = useState('')
|
|
569
1012
|
const domains = [...new Set((papers || []).flatMap((p) => p.tags.filter((t) => t.startsWith('领域/')).map((t) => t.slice(3))))].sort((a, b) => a.localeCompare(b, 'zh'))
|
|
570
1013
|
const items = (papers || []).filter((p) => {
|
|
571
1014
|
if (onlyPending && !p.pending) return false
|
|
@@ -573,93 +1016,85 @@ window.__ModuleLoader__.load({
|
|
|
573
1016
|
if (!q) return true
|
|
574
1017
|
return `${p.title} ${p.year} ${p.journal} ${p.tags.join(' ')}`.toLowerCase().includes(q.toLowerCase())
|
|
575
1018
|
})
|
|
576
|
-
const rows = items.map((p) =>
|
|
577
|
-
|
|
1019
|
+
const rows = items.map((p) => {
|
|
1020
|
+
const on = selected === p.dir
|
|
1021
|
+
const dom = domainOfPaper(p)
|
|
1022
|
+
const domColor = domainColor(dom, domains)
|
|
1023
|
+
const meta = p.journal || dom || '—'
|
|
1024
|
+
return h(
|
|
578
1025
|
'div',
|
|
579
1026
|
{
|
|
580
1027
|
key: p.dir,
|
|
581
1028
|
onClick: () => onSelect(p.dir),
|
|
1029
|
+
onMouseEnter: () => setHovered(p.dir),
|
|
1030
|
+
onMouseLeave: () => setHovered(''),
|
|
582
1031
|
title: p.title,
|
|
583
1032
|
style: {
|
|
584
|
-
|
|
585
|
-
borderRadius:
|
|
586
|
-
|
|
587
|
-
background: selected === p.dir ? 'rgba(77,107,254,0.16)' : card,
|
|
588
|
-
border: `1px solid ${selected === p.dir ? 'rgba(77,107,254,0.45)' : line}`,
|
|
1033
|
+
position: 'relative', display: 'grid', gridTemplateColumns: '38px 1fr', gap: 10,
|
|
1034
|
+
padding: '10px 10px 11px', borderRadius: 8, cursor: 'pointer',
|
|
1035
|
+
background: on ? 'rgba(77,107,254,0.12)' : hovered === p.dir ? hoverBg : 'transparent',
|
|
589
1036
|
},
|
|
590
1037
|
},
|
|
591
|
-
h(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
h('
|
|
595
|
-
h('span', { style: { flex: '1 1 auto', fontSize: FS.body, fontWeight: 550, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, p.title),
|
|
596
|
-
p.pending ? h('span', { style: { color: WARN_TEXT, flex: '0 0 auto', fontSize: FS.caption } }, '待总结') : null,
|
|
597
|
-
),
|
|
598
|
-
h('div', {
|
|
599
|
-
style: {
|
|
600
|
-
marginTop: 4, fontSize: FS.caption, color: muted, lineHeight: 1.5,
|
|
601
|
-
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
|
602
|
-
},
|
|
603
|
-
}, (p.summary || '').replace(/^>\s*/, '') || (p.tags || []).slice(0, 5).join(' · ')),
|
|
604
|
-
),
|
|
605
|
-
)
|
|
606
|
-
const left = h(
|
|
607
|
-
'div',
|
|
608
|
-
{ style: { borderRight: `1px solid ${line}`, overflowY: 'auto', minHeight: 0, padding: '14px 10px 16px' } },
|
|
609
|
-
h('input', {
|
|
610
|
-
value: q,
|
|
611
|
-
onChange: (e) => setQ(e.target.value),
|
|
612
|
-
placeholder: '搜索标题 / 年份 / 期刊 / 标签…',
|
|
613
|
-
style: {
|
|
614
|
-
width: '100%', boxSizing: 'border-box', height: 34, padding: '0 11px',
|
|
615
|
-
borderRadius: R.sm, border: `1px solid ${line}`, background: T.bg1,
|
|
616
|
-
color: 'inherit', fontSize: FS.body, marginBottom: SP.sm, outline: 'none',
|
|
617
|
-
},
|
|
618
|
-
}),
|
|
619
|
-
h(
|
|
620
|
-
'div',
|
|
621
|
-
{ style: { display: 'flex', gap: SP.sm, alignItems: 'center', marginBottom: SP.sm } },
|
|
622
|
-
h(
|
|
623
|
-
'select',
|
|
624
|
-
{
|
|
625
|
-
value: domain,
|
|
626
|
-
onChange: (e) => setDomain(e.target.value),
|
|
1038
|
+
on ? h('div', { style: { position: 'absolute', left: 0, top: 9, bottom: 9, width: 2, borderRadius: 2, background: ACCENT } }) : null,
|
|
1039
|
+
h('div', { style: { fontFamily: mono, fontSize: 12, color: T.text3, paddingTop: 2, fontVariantNumeric: 'tabular-nums' } }, p.year || '—'),
|
|
1040
|
+
h('div', { style: { minWidth: 0 } },
|
|
1041
|
+
h('div', {
|
|
627
1042
|
style: {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
color: 'inherit', fontSize: FS.caption,
|
|
1043
|
+
fontSize: 13.5, lineHeight: 1.45, fontWeight: 550, display: '-webkit-box',
|
|
1044
|
+
WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
|
631
1045
|
},
|
|
1046
|
+
}, p.title),
|
|
1047
|
+
h('div', { style: { marginTop: 4, display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, fontSize: 12, color: T.caption } },
|
|
1048
|
+
h('span', { style: { flex: '1 1 auto', display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0 } },
|
|
1049
|
+
h('span', { style: { flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 5, color: dom ? domColor : T.caption } },
|
|
1050
|
+
h('span', { style: { width: 5, height: 5, borderRadius: '50%', background: dom ? domColor : 'transparent' } }), dom || '—'),
|
|
1051
|
+
h('span', { style: { flex: '1 1 auto', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, p.journal || '')),
|
|
1052
|
+
p.pending
|
|
1053
|
+
? h('span', { style: { flex: '0 0 auto', color: ORANGE, display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
1054
|
+
h('span', { style: { width: 5, height: 5, borderRadius: '50%', background: 'currentColor' } }), '待总结')
|
|
1055
|
+
: null)))
|
|
1056
|
+
})
|
|
1057
|
+
const left = h(
|
|
1058
|
+
'div',
|
|
1059
|
+
{ style: { minHeight: 0, display: 'flex', flexDirection: 'column', background: T.bg1, borderRight: `1px solid ${line}` } },
|
|
1060
|
+
h(RailHead, { title: '论文', count: fmt((papers || []).length) }),
|
|
1061
|
+
h(RailSearch, { value: q, onChange: setQ, placeholder: '搜索标题 / 年份 / 期刊 / 标签' }),
|
|
1062
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px 10px' } },
|
|
1063
|
+
h('select', {
|
|
1064
|
+
value: domain,
|
|
1065
|
+
onChange: (e) => setDomain(e.target.value),
|
|
1066
|
+
style: {
|
|
1067
|
+
flex: '1 1 auto', minWidth: 0, height: 26, padding: '0 8px', border: 'none',
|
|
1068
|
+
borderRadius: 6, background: T.bg2, color: T.text2, fontSize: 12.5,
|
|
632
1069
|
},
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
'
|
|
637
|
-
|
|
638
|
-
h('input', { type: 'checkbox', checked: onlyPending, onChange: (e) => setOnlyPending(e.target.checked) }),
|
|
639
|
-
'只看待补',
|
|
640
|
-
),
|
|
641
|
-
),
|
|
642
|
-
h('div', { style: { fontSize: FS.caption, color: faint, marginBottom: SP.sm } }, `显示 ${items.length} / ${(papers || []).length} 篇`),
|
|
1070
|
+
}, [h('option', { key: '', value: '' }, '全部领域')].concat(domains.map((d) => h('option', { key: d, value: d }, d)))),
|
|
1071
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, color: T.text2, cursor: 'pointer', whiteSpace: 'nowrap', fontSize: 12.5 } },
|
|
1072
|
+
h('input', { type: 'checkbox', checked: onlyPending, onChange: (e) => setOnlyPending(e.target.checked), style: { accentColor: ACCENT, margin: 0 } }),
|
|
1073
|
+
'只看待补')),
|
|
1074
|
+
h('div', { style: { padding: '0 16px 8px', fontSize: 12, color: T.caption, fontVariantNumeric: 'tabular-nums' } }, `显示 ${items.length} / ${(papers || []).length} 篇`),
|
|
643
1075
|
loading
|
|
644
|
-
? h('div', { style: { color: muted, fontSize: FS.body } }, '加载中…')
|
|
645
|
-
: h('div', { style: {
|
|
1076
|
+
? h('div', { style: { color: muted, fontSize: FS.body, padding: '8px 16px' } }, '加载中…')
|
|
1077
|
+
: h('div', { style: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', padding: '0 8px 32px' } }, rows),
|
|
646
1078
|
)
|
|
647
1079
|
const right = h(
|
|
648
1080
|
'div',
|
|
649
|
-
{ style: {
|
|
650
|
-
selectedData && selectedData.pending
|
|
651
|
-
? h(
|
|
652
|
-
'div',
|
|
653
|
-
{ style: { display: 'flex', alignItems: 'center', gap: SP.md, marginBottom: SP.md, padding: '10px 12px', borderRadius: R.md, background: 'rgba(196,123,30,0.12)', border: '1px solid rgba(196,123,30,0.35)' } },
|
|
654
|
-
h('span', { style: { fontSize: FS.body, flex: '1 1 auto' } }, '这篇条目还没有摘要与标签'),
|
|
655
|
-
h(Btn, { primary: true, disabled: running, onClick: () => onSummarize(selected) }, running ? '处理中…' : '生成摘要与标签'),
|
|
656
|
-
)
|
|
657
|
-
: null,
|
|
1081
|
+
{ style: { minHeight: 0, overflowY: 'auto' } },
|
|
658
1082
|
selected && selectedData
|
|
659
|
-
? h(
|
|
660
|
-
|
|
1083
|
+
? h(ReaderPane, null,
|
|
1084
|
+
selectedData.pending
|
|
1085
|
+
? h('div', {
|
|
1086
|
+
style: {
|
|
1087
|
+
display: 'flex', alignItems: 'center', gap: SP.md, marginBottom: 20,
|
|
1088
|
+
padding: '10px 14px', borderRadius: 8, background: 'rgba(196,123,30,0.10)', border: '1px solid rgba(196,123,30,0.30)',
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
h('span', { style: { fontSize: FS.body, flex: '1 1 auto' } }, '这篇条目还没有摘要与标签'),
|
|
1092
|
+
h(Btn, { small: true, primary: true, disabled: running, onClick: () => onSummarize(selected) }, running ? '处理中…' : '生成摘要与标签'))
|
|
1093
|
+
: null,
|
|
1094
|
+
h(PaperPreview, { data: selectedData, onOpenPaper: onSelect }))
|
|
1095
|
+
: h(ReaderEmpty, { title: '从左侧选一篇论文', sub: '可搜索、按领域筛选、只看待补;条目内容按摘要排版显示' }),
|
|
661
1096
|
)
|
|
662
|
-
return h('div', { style: { display: 'grid', gridTemplateColumns: '
|
|
1097
|
+
return h('div', { style: { display: 'grid', gridTemplateColumns: '336px 1fr', height: '100%', minHeight: 0 } }, left, right)
|
|
663
1098
|
}
|
|
664
1099
|
|
|
665
1100
|
// obsidian:// 只能由宿主侧拉起:桌面版对自定义协议的 window.open 一律 deny
|
|
@@ -673,9 +1108,10 @@ window.__ModuleLoader__.load({
|
|
|
673
1108
|
return await r.json()
|
|
674
1109
|
}
|
|
675
1110
|
|
|
676
|
-
function PaperPreview({ data,
|
|
1111
|
+
function PaperPreview({ data, onOpenPaper }) {
|
|
677
1112
|
const [msg, setMsg] = useState('')
|
|
678
1113
|
if (!data) return null
|
|
1114
|
+
const fm = parsePaperFrontmatter(data.index)
|
|
679
1115
|
const rel = `1-Papers/${data.dir}/index.md`
|
|
680
1116
|
const copyPath = () => {
|
|
681
1117
|
try {
|
|
@@ -697,61 +1133,37 @@ window.__ModuleLoader__.load({
|
|
|
697
1133
|
setMsg('打开失败:' + String(e) + ',已复制条目路径')
|
|
698
1134
|
}
|
|
699
1135
|
}
|
|
700
|
-
const
|
|
701
|
-
.
|
|
1136
|
+
const meta = [
|
|
1137
|
+
{ label: '年份', value: fm.year },
|
|
1138
|
+
{ label: '期刊/会议', value: fm.journal },
|
|
1139
|
+
{ label: 'DOI', value: fm.doi || (fm.arxiv ? 'arXiv ' + fm.arxiv : '') },
|
|
1140
|
+
{ label: '篇幅', value: [fm.pages && fm.pages + ' 页', fm.figures && fm.figures + ' 图'].filter(Boolean).join(' · ') },
|
|
1141
|
+
{ label: '引用键', value: fm.citekey },
|
|
1142
|
+
]
|
|
1143
|
+
const concepts = data.concepts || []
|
|
1144
|
+
const taxonomy = (data.taxonomy || []).slice().sort((a, b) => {
|
|
1145
|
+
const order = ['领域', '方法', '应用', '仪器']
|
|
1146
|
+
return order.indexOf(tagCategory(a)) - order.indexOf(tagCategory(b))
|
|
1147
|
+
})
|
|
702
1148
|
return h(
|
|
703
1149
|
'div',
|
|
704
1150
|
null,
|
|
705
|
-
h(
|
|
706
|
-
'div',
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
),
|
|
1151
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 16 } },
|
|
1152
|
+
h('div', { style: { flex: '1 1 auto', fontSize: FS.page, lineHeight: 1.4, fontWeight: 650 } }, fm.title || data.dir),
|
|
1153
|
+
h(DocActions, null,
|
|
1154
|
+
h(Btn, { small: true, ghost: true, onClick: copyPath, title: '复制库内相对路径' }, '复制路径'),
|
|
1155
|
+
h(Btn, { small: true, onClick: openInObsidian, title: '用 Obsidian 打开这篇笔记' }, '在 Obsidian 打开'))),
|
|
1156
|
+
h(MetaStrip, { items: meta }),
|
|
712
1157
|
msg ? h('div', { style: { fontSize: FS.caption, color: ERROR_TEXT, marginTop: SP.xs } }, msg) : null,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
? h(
|
|
718
|
-
'div',
|
|
719
|
-
{ style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: SP.sm } },
|
|
720
|
-
data.concepts.map((c) =>
|
|
721
|
-
h('span', { key: c, style: { fontSize: FS.caption, padding: '3px 10px', borderRadius: 999, border: `1px solid ${line}`, background: 'rgba(167,139,250,0.10)' } }, '概念 · ' + c),
|
|
722
|
-
),
|
|
723
|
-
)
|
|
1158
|
+
concepts.length || taxonomy.length
|
|
1159
|
+
? h('div', { style: { marginTop: 16, display: 'flex', flexWrap: 'wrap', gap: 6 } },
|
|
1160
|
+
concepts.map((c) => h(ConceptChip, { key: 'c' + c, name: c })),
|
|
1161
|
+
taxonomy.map((t) => h(TaxChip, { key: 't' + t, id: t })))
|
|
724
1162
|
: null,
|
|
725
|
-
data.
|
|
726
|
-
? h(
|
|
727
|
-
'div',
|
|
728
|
-
{ style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 6 } },
|
|
729
|
-
data.taxonomy.map((c) =>
|
|
730
|
-
h('span', { key: c, style: { fontSize: FS.caption, padding: '3px 10px', borderRadius: 999, border: '1px solid rgba(255,215,0,0.35)', background: 'rgba(255,215,0,0.06)' } }, '分类 · ' + c),
|
|
731
|
-
),
|
|
732
|
-
)
|
|
733
|
-
: null,
|
|
734
|
-
h(
|
|
735
|
-
'pre',
|
|
736
|
-
{
|
|
737
|
-
style: {
|
|
738
|
-
marginTop: SP.md, fontSize: FS.body, lineHeight: 1.85, whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
739
|
-
background: card, border: `1px solid ${line}`, borderRadius: R.lg, padding: '16px 18px',
|
|
740
|
-
fontFamily: 'inherit', color: 'inherit', maxHeight: 'none',
|
|
741
|
-
},
|
|
742
|
-
},
|
|
743
|
-
data.index || '',
|
|
744
|
-
),
|
|
1163
|
+
h('div', { style: { marginTop: 26 } }, h(Markdown, { source: data.index, ledeHeading: '一句话总结', onOpenPaper })),
|
|
745
1164
|
)
|
|
746
1165
|
}
|
|
747
1166
|
|
|
748
|
-
const DOMAIN_COLORS = ['#f472b6', '#60a5fa', '#34d399', '#fbbf24', '#a78bfa', '#38bdf8', '#fb923c', '#4ade80', '#e879f9', '#22d3ee', '#f87171', '#a3e635']
|
|
749
|
-
|
|
750
|
-
function domainColor(domain, domains) {
|
|
751
|
-
const i = Math.max(0, domains.indexOf(domain))
|
|
752
|
-
return DOMAIN_COLORS[i % DOMAIN_COLORS.length]
|
|
753
|
-
}
|
|
754
|
-
|
|
755
1167
|
function layoutGraph(data) {
|
|
756
1168
|
const W = 1000
|
|
757
1169
|
const H = 660
|
|
@@ -883,7 +1295,7 @@ window.__ModuleLoader__.load({
|
|
|
883
1295
|
dots.push(h('span', { key: 'tax', style: { display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: FS.caption, color: muted } },
|
|
884
1296
|
h('span', { style: { width: 8, height: 8, borderRadius: 999, background: '#ffd700' } }), '分类'))
|
|
885
1297
|
dots.push(h('span', { key: 'con', style: { display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: FS.caption, color: muted } },
|
|
886
|
-
h('span', { style: { width: 8, height: 8, borderRadius: 999, background:
|
|
1298
|
+
h('span', { style: { width: 8, height: 8, borderRadius: 999, background: CONCEPT_COLOR } }), '概念'))
|
|
887
1299
|
|
|
888
1300
|
const graph = (data.papers.length + (data.concepts || []).length) === 0
|
|
889
1301
|
? h('div', { style: { height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: faint, fontSize: FS.body } }, '还没有论文或概念,导入几篇后这里会出现关系图')
|
|
@@ -916,7 +1328,7 @@ window.__ModuleLoader__.load({
|
|
|
916
1328
|
? !domain || ((linked && linked.papers) || []).some((d) => visible(paperById.get(d) || { id: d }))
|
|
917
1329
|
: visible(p) || (!focus && !domain)
|
|
918
1330
|
const dim = focusId ? !focusNeighbors.has(n.id) : !vis
|
|
919
|
-
const fill = isConcept ?
|
|
1331
|
+
const fill = isConcept ? CONCEPT_COLOR : isTax ? '#d5a94a' : domainColor(p.domain || '其他', domains)
|
|
920
1332
|
const deg = sim.degree.get(sim.nodes.indexOf(n)) || 0
|
|
921
1333
|
const r = isConcept ? 10 + Math.min(6, ((n.ref.papers || []).length || 0) / 8)
|
|
922
1334
|
: isTax ? 6 + Math.min(5, deg / 3)
|
|
@@ -983,6 +1395,8 @@ window.__ModuleLoader__.load({
|
|
|
983
1395
|
const [active, setActive] = useState('')
|
|
984
1396
|
const [detail, setDetail] = useState(null)
|
|
985
1397
|
const [msg, setMsg] = useState('')
|
|
1398
|
+
const [q, setQ] = useState('')
|
|
1399
|
+
const [hovered, setHovered] = useState('')
|
|
986
1400
|
|
|
987
1401
|
const rel = detail ? `2-Concepts/${detail.name}.md` : ''
|
|
988
1402
|
const openInObsidian = async () => {
|
|
@@ -1031,67 +1445,100 @@ window.__ModuleLoader__.load({
|
|
|
1031
1445
|
}
|
|
1032
1446
|
}
|
|
1033
1447
|
|
|
1448
|
+
const filtered = (items || []).filter((c) => {
|
|
1449
|
+
if (!q) return true
|
|
1450
|
+
const hay = `${c.name} ${c.title} ${(c.aliases || []).join(' ')} ${c.oneLiner || ''}`.toLowerCase()
|
|
1451
|
+
return hay.includes(q.toLowerCase())
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
const rows = filtered.map((c) => {
|
|
1455
|
+
const on = active === c.name
|
|
1456
|
+
return h(
|
|
1457
|
+
'div',
|
|
1458
|
+
{
|
|
1459
|
+
key: c.name,
|
|
1460
|
+
onClick: () => open(c.name),
|
|
1461
|
+
onMouseEnter: () => setHovered(c.name),
|
|
1462
|
+
onMouseLeave: () => setHovered(''),
|
|
1463
|
+
title: c.oneLiner || c.title,
|
|
1464
|
+
style: {
|
|
1465
|
+
position: 'relative', display: 'grid', gridTemplateColumns: '1fr auto', gap: 10,
|
|
1466
|
+
padding: '10px 10px 11px', borderRadius: 8, cursor: 'pointer',
|
|
1467
|
+
background: on ? 'rgba(77,107,254,0.12)' : hovered === c.name ? hoverBg : 'transparent',
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
on ? h('div', { style: { position: 'absolute', left: 0, top: 9, bottom: 9, width: 2, borderRadius: 2, background: CONCEPT_COLOR } }) : null,
|
|
1471
|
+
h('div', { style: { minWidth: 0 } },
|
|
1472
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 } },
|
|
1473
|
+
h('span', { style: { width: 6, height: 6, borderRadius: '50%', background: CONCEPT_COLOR, flex: '0 0 auto' } }),
|
|
1474
|
+
h('div', { style: { fontSize: 14, fontWeight: 550, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.title || c.name)),
|
|
1475
|
+
c.oneLiner
|
|
1476
|
+
? h('div', { style: { marginTop: 3, fontSize: 12, color: T.caption, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', paddingLeft: 13 } }, c.oneLiner)
|
|
1477
|
+
: null),
|
|
1478
|
+
h('div', { style: { fontFamily: mono, fontSize: 12, color: hexA(CONCEPT_COLOR, 0.75), fontVariantNumeric: 'tabular-nums', paddingTop: 3 } }, String(c.papersCount || 0)))
|
|
1479
|
+
})
|
|
1480
|
+
|
|
1034
1481
|
const left = h(
|
|
1035
1482
|
'div',
|
|
1036
|
-
{ style: {
|
|
1483
|
+
{ style: { minHeight: 0, display: 'flex', flexDirection: 'column', background: T.bg1, borderRight: `1px solid ${line}` } },
|
|
1484
|
+
h(RailHead, { title: '概念', count: items ? fmt(items.length) : '—' }),
|
|
1485
|
+
h(RailSearch, { value: q, onChange: setQ, placeholder: '搜索概念 / 别名' }),
|
|
1486
|
+
h('div', { style: { padding: '0 16px 8px', fontSize: 12, color: T.caption } }, '按引用论文数排序'),
|
|
1037
1487
|
items
|
|
1038
|
-
? h(
|
|
1039
|
-
|
|
1040
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
1041
|
-
items.map((c) =>
|
|
1042
|
-
h(
|
|
1043
|
-
'div',
|
|
1044
|
-
{
|
|
1045
|
-
key: c.name,
|
|
1046
|
-
onClick: () => open(c.name),
|
|
1047
|
-
title: c.oneLiner || c.title,
|
|
1048
|
-
style: {
|
|
1049
|
-
padding: '9px 11px',
|
|
1050
|
-
borderRadius: R.sm,
|
|
1051
|
-
cursor: 'pointer',
|
|
1052
|
-
fontSize: FS.body,
|
|
1053
|
-
background: active === c.name ? 'rgba(77,107,254,0.16)' : card,
|
|
1054
|
-
border: `1px solid ${active === c.name ? 'rgba(77,107,254,0.45)' : line}`,
|
|
1055
|
-
},
|
|
1056
|
-
},
|
|
1057
|
-
h('div', { style: { display: 'flex', gap: 6 } },
|
|
1058
|
-
h('span', { style: { flex: '1 1 auto' } }, c.title),
|
|
1059
|
-
h('span', { style: { color: muted, flex: '0 0 auto', fontSize: FS.caption } }, c.papersCount + ' 篇')),
|
|
1060
|
-
h('div', { style: { fontSize: FS.caption, color: muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 2 } }, c.oneLiner || ''),
|
|
1061
|
-
),
|
|
1062
|
-
),
|
|
1063
|
-
)
|
|
1064
|
-
: h('div', { style: { color: muted, fontSize: FS.body } }, '加载中…'),
|
|
1488
|
+
? h('div', { style: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', padding: '0 8px 32px' } }, rows)
|
|
1489
|
+
: h('div', { style: { color: muted, fontSize: FS.body, padding: '8px 16px' } }, '加载中…'),
|
|
1065
1490
|
)
|
|
1066
1491
|
|
|
1492
|
+
const fm = detail ? parsePaperFrontmatter(detail.markdown) : {}
|
|
1493
|
+
const tags = (fm.tags || []).filter((t) => !t.startsWith('状态/'))
|
|
1494
|
+
const related = (detail && detail.papers ? detail.papers.slice() : []).sort((a, b) => (parseFloat(b.year || '0') || 0) - (parseFloat(a.year || '0') || 0))
|
|
1067
1495
|
const right = h(
|
|
1068
1496
|
'div',
|
|
1069
|
-
{ style: {
|
|
1497
|
+
{ style: { minHeight: 0, overflowY: 'auto' } },
|
|
1070
1498
|
detail
|
|
1071
|
-
? h(
|
|
1072
|
-
'div',
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
h('div', { style: {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1499
|
+
? h(ReaderPane, null,
|
|
1500
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 16 } },
|
|
1501
|
+
h('div', { style: { flex: '1 1 auto', fontSize: FS.page, lineHeight: 1.4, fontWeight: 650 } }, detail.title || detail.name),
|
|
1502
|
+
h(DocActions, null,
|
|
1503
|
+
h(Btn, { small: true, ghost: true, onClick: copyPath, title: '复制库内相对路径' }, '复制路径'),
|
|
1504
|
+
h(Btn, { small: true, onClick: openInObsidian, title: '用 Obsidian 打开概念页' }, '在 Obsidian 打开'))),
|
|
1505
|
+
h(MetaStrip, {
|
|
1506
|
+
items: [
|
|
1507
|
+
{ label: '类型', value: '概念页' },
|
|
1508
|
+
{ label: '引用', value: (detail.papersCount || 0) + ' 篇论文' },
|
|
1509
|
+
{ label: '别名', value: (fm.aliases || detail.aliases || []).slice(0, 4).join(' · ') },
|
|
1510
|
+
{ label: '更新', value: fm.updated },
|
|
1511
|
+
],
|
|
1512
|
+
}),
|
|
1513
|
+
msg ? h('div', { style: { fontSize: FS.caption, color: ERROR_TEXT, marginTop: SP.xs } }, msg) : null,
|
|
1514
|
+
tags.length
|
|
1515
|
+
? h('div', { style: { marginTop: 16, display: 'flex', flexWrap: 'wrap', gap: 6 } }, tags.map((t) => h(TaxChip, { key: t, id: t })))
|
|
1516
|
+
: null,
|
|
1517
|
+
h('div', { style: { marginTop: 26 } }, h(Markdown, { source: detail.markdown, ledeFirstQuote: true, onOpenPaper })),
|
|
1518
|
+
related.length
|
|
1519
|
+
? h('div', null,
|
|
1520
|
+
h(H2, null, '相关论文'),
|
|
1521
|
+
h('div', null, related.map((p) => h('div', {
|
|
1522
|
+
key: p.dir,
|
|
1523
|
+
onClick: () => onOpenPaper(p.dir),
|
|
1524
|
+
title: p.title,
|
|
1525
|
+
onMouseEnter: () => setHovered('paper:' + p.dir),
|
|
1526
|
+
onMouseLeave: () => setHovered(''),
|
|
1527
|
+
style: {
|
|
1528
|
+
display: 'grid', gridTemplateColumns: '42px 1fr', gap: 12, padding: '9px 8px', borderRadius: 7,
|
|
1529
|
+
cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.045)',
|
|
1530
|
+
background: hovered === 'paper:' + p.dir ? hoverBg : 'transparent',
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
h('div', { style: { fontFamily: mono, fontSize: 12, color: T.text3, paddingTop: 2, fontVariantNumeric: 'tabular-nums' } }, p.year || '—'),
|
|
1534
|
+
h('div', { style: { fontSize: 13.5, lineHeight: 1.5 } }, p.title)))))
|
|
1535
|
+
: null)
|
|
1536
|
+
: h(ReaderEmpty, {
|
|
1537
|
+
title: items && items.length ? '从左侧选一个概念' : '还没有概念页',
|
|
1538
|
+
sub: items && items.length ? '概念页是跨论文综合,条目内容按摘要排版显示' : '概念页由 tools/ 下的素材、校验、索引脚本生成,规范见 config/CONCEPT_SPEC.md',
|
|
1539
|
+
}),
|
|
1092
1540
|
)
|
|
1093
|
-
|
|
1094
|
-
return h('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start' } }, left, right)
|
|
1541
|
+
return h('div', { style: { display: 'grid', gridTemplateColumns: '300px 1fr', height: '100%', minHeight: 0 } }, left, right)
|
|
1095
1542
|
}
|
|
1096
1543
|
|
|
1097
1544
|
function PaperDepotView() {
|
|
@@ -1107,6 +1554,8 @@ window.__ModuleLoader__.load({
|
|
|
1107
1554
|
const [setup, setSetup] = useState(null)
|
|
1108
1555
|
const [showSetup, setShowSetup] = useState(false)
|
|
1109
1556
|
const [showImport, setShowImport] = useState(false)
|
|
1557
|
+
const [jobInfo, setJobInfo] = useState(null)
|
|
1558
|
+
|
|
1110
1559
|
|
|
1111
1560
|
const loadSetup = useCallback(async () => {
|
|
1112
1561
|
try {
|
|
@@ -1153,11 +1602,13 @@ window.__ModuleLoader__.load({
|
|
|
1153
1602
|
|
|
1154
1603
|
const startPolling = useCallback(async () => {
|
|
1155
1604
|
setRunning(true)
|
|
1156
|
-
|
|
1605
|
+
// 批量补摘要可能跑几个小时:轮询上限放到 ~3 小时,任务结束后立即退出
|
|
1606
|
+
for (let i = 0; i < 9000; i++) {
|
|
1157
1607
|
await new Promise((r) => setTimeout(r, 1200))
|
|
1158
1608
|
try {
|
|
1159
1609
|
const j = await (await api('/job')).json()
|
|
1160
1610
|
if (j.job) {
|
|
1611
|
+
setJobInfo(j.job)
|
|
1161
1612
|
setLog((j.job.lines || []).slice(-300))
|
|
1162
1613
|
if (j.job.state !== 'running') {
|
|
1163
1614
|
setRunning(false)
|
|
@@ -1255,6 +1706,33 @@ window.__ModuleLoader__.load({
|
|
|
1255
1706
|
}
|
|
1256
1707
|
}, [startPolling, openPaper, setLog])
|
|
1257
1708
|
|
|
1709
|
+
// 批量补摘要:排队把「待生成」的条目跑完;中断后再点会从剩下的继续
|
|
1710
|
+
const onSummarizePending = useCallback(async () => {
|
|
1711
|
+
setJobInfo(null)
|
|
1712
|
+
try {
|
|
1713
|
+
const r = await fetch('/paperdepot/api/summarize-pending', { method: 'POST', credentials: 'same-origin' })
|
|
1714
|
+
const j = await r.json()
|
|
1715
|
+
if (!j.ok) {
|
|
1716
|
+
setLog([`❌ ${j.error || r.status}`])
|
|
1717
|
+
return
|
|
1718
|
+
}
|
|
1719
|
+
setLog([`▶ 批量补摘要:排队 ${j.total} 篇(库里待生成 ${j.pending} 篇)`])
|
|
1720
|
+
await startPolling()
|
|
1721
|
+
} catch (e) {
|
|
1722
|
+
setLog([`❌ ${String(e)}`])
|
|
1723
|
+
}
|
|
1724
|
+
}, [startPolling, setLog])
|
|
1725
|
+
|
|
1726
|
+
const onCancelJob = useCallback(async () => {
|
|
1727
|
+
try {
|
|
1728
|
+
const r = await fetch('/paperdepot/api/job-cancel', { method: 'POST', credentials: 'same-origin' })
|
|
1729
|
+
const j = await r.json()
|
|
1730
|
+
if (!j.ok) setLog((old) => [...old, `❌ ${j.error || r.status}`])
|
|
1731
|
+
} catch (e) {
|
|
1732
|
+
setLog((old) => [...old, `❌ ${String(e)}`])
|
|
1733
|
+
}
|
|
1734
|
+
}, [setLog])
|
|
1735
|
+
|
|
1258
1736
|
const onSetupSaved = useCallback(async () => {
|
|
1259
1737
|
try {
|
|
1260
1738
|
const s = await (await api('/setup/status')).json()
|
|
@@ -1301,11 +1779,11 @@ window.__ModuleLoader__.load({
|
|
|
1301
1779
|
['concepts', '概念', d ? String(d.concepts) : ''],
|
|
1302
1780
|
]
|
|
1303
1781
|
const content = setup && !setup.hasKey
|
|
1304
|
-
? h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px
|
|
1782
|
+
? h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px 72px', boxSizing: 'border-box' } }, h(SetupCard, { setup, onSaved: onSetupSaved }))
|
|
1305
1783
|
: d
|
|
1306
1784
|
? tab === 'papers'
|
|
1307
1785
|
? h(Papers, { papers, loading: papersLoading, onSelect: openPaper, selected, selectedData, onSummarize, running, vaultName })
|
|
1308
|
-
: h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px
|
|
1786
|
+
: h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px 72px', boxSizing: 'border-box' } },
|
|
1309
1787
|
tab === 'overview'
|
|
1310
1788
|
? h(Overview, { d, onOpen: openFromGraph })
|
|
1311
1789
|
: h(Concepts, { onOpenPaper: openFromGraph, vaultName }))
|
|
@@ -1404,13 +1882,31 @@ window.__ModuleLoader__.load({
|
|
|
1404
1882
|
style: { fontSize: 12.5, color: muted, background: T.bg2, padding: '1px 6px', borderRadius: 999, lineHeight: 1.5 },
|
|
1405
1883
|
}, count) : null)),
|
|
1406
1884
|
h('div', { style: { flex: '1 1 auto' } }),
|
|
1885
|
+
d && d.pendingSummary
|
|
1886
|
+
? h(Btn, {
|
|
1887
|
+
small: true,
|
|
1888
|
+
ghost: true,
|
|
1889
|
+
onClick: onSummarizePending,
|
|
1890
|
+
disabled: running,
|
|
1891
|
+
title: '把「待生成」的条目排队跑完;可随时停止,下次再点会跳过已完成的',
|
|
1892
|
+
}, running
|
|
1893
|
+
? `任务中(${jobInfo && jobInfo.kind === 'summarize-batch' ? `${jobInfo.done || 0}/${jobInfo.total || 0}` : '…'})`
|
|
1894
|
+
: `批量补摘要(${d.pendingSummary})`)
|
|
1895
|
+
: null,
|
|
1407
1896
|
h(Btn, { small: true, ghost: true, onClick: onRebuildNav, disabled: running, title: '重建论文导航块、分类页与图颜色分组' }, running ? '重建中…' : '重建导航')),
|
|
1408
1897
|
// 内容
|
|
1409
1898
|
h('div', { style: { flex: '1 1 auto', minHeight: 0, position: 'relative' } }, content),
|
|
1899
|
+
// 底部安全区:flex 末尾的固定占位(等价 HarmonyOS 的 bottom safe area / Blank),
|
|
1900
|
+
// 让上方内容整体上移,不贴窗口下沿、不被任务栏压住;不放滚动区里,避免随内容滚走
|
|
1901
|
+
h('div', { style: { flex: '0 0 auto', height: 24 } }),
|
|
1410
1902
|
showSetup && setup && setup.hasKey
|
|
1411
1903
|
? h('div', { style: { position: 'absolute', top: 58, right: 20, zIndex: 15, width: 540, maxWidth: '92vw', maxHeight: 'calc(100% - 80px)', overflowY: 'auto' } },
|
|
1412
1904
|
h(SetupCard, { setup, onSaved: onSetupSaved }))
|
|
1413
1905
|
: null,
|
|
1906
|
+
// 批量补摘要进度条:任务结束时保留,可收起
|
|
1907
|
+
jobInfo && jobInfo.kind === 'summarize-batch'
|
|
1908
|
+
? h(BatchBar, { job: jobInfo, onCancel: onCancelJob, onClose: () => setJobInfo(null) })
|
|
1909
|
+
: null,
|
|
1414
1910
|
h(ImportDialog, { open: showImport, onClose: () => setShowImport(false), running, log, uploadFiles }),
|
|
1415
1911
|
)
|
|
1416
1912
|
}
|