paperdepot 0.1.1 → 0.1.2
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 +12 -5
- package/README.md +25 -22
- package/lib/client.js +663 -179
- package/lib/impl.js +65 -0
- 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/mineru_parse.py +462 -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 || '')
|
|
@@ -519,14 +919,14 @@ window.__ModuleLoader__.load({
|
|
|
519
919
|
}
|
|
520
920
|
|
|
521
921
|
function Overview({ d, onOpen }) {
|
|
522
|
-
const tagItems = (d.tags || []).slice(0, 14).map((t) => ({ key: t.tag, label: t.tag, value: t.count }))
|
|
922
|
+
const tagItems = (d.tags || []).slice(0, 14).map((t) => ({ key: t.tag, label: t.tag, value: t.count, color: tagColor(t.tag) }))
|
|
523
923
|
const yearItems = (d.years || []).slice(-14).map((y) => ({ key: String(y.year), label: String(y.year), value: y.count }))
|
|
524
924
|
return h(
|
|
525
925
|
'div',
|
|
526
926
|
null,
|
|
527
927
|
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: SP.md } },
|
|
528
928
|
h(Card, { label: '论文条目', value: fmt(d.papers) }),
|
|
529
|
-
h(Card, { label: '概念页', value: fmt(d.concepts) }),
|
|
929
|
+
h(Card, { label: '概念页', value: fmt(d.concepts), accent: d.concepts ? CONCEPT_COLOR : undefined }),
|
|
530
930
|
h(Card, { label: '待补元数据', value: fmt(d.pendingMeta), accent: d.pendingMeta ? WARN_TEXT : undefined }),
|
|
531
931
|
h(Card, { label: '待生成摘要', value: fmt(d.pendingSummary), accent: d.pendingSummary ? WARN_TEXT : undefined, sub: d.pendingSummary ? '导入后未跑摘要' : '全部齐全' }),
|
|
532
932
|
),
|
|
@@ -557,15 +957,46 @@ window.__ModuleLoader__.load({
|
|
|
557
957
|
(d.conceptNames || []).length
|
|
558
958
|
? h(Section, { title: `概念层(${d.concepts} 页)` },
|
|
559
959
|
h('div', { style: { display: 'flex', gap: SP.sm, flexWrap: 'wrap' } },
|
|
560
|
-
d.conceptNames.map((n) => h(
|
|
960
|
+
d.conceptNames.map((n) => h(ConceptChip, { key: n, name: n }))))
|
|
561
961
|
: null,
|
|
562
962
|
)
|
|
563
963
|
}
|
|
564
964
|
|
|
965
|
+
// 批量补摘要进度条:贴在内容区底部,任务结束后保留可收起
|
|
966
|
+
function BatchBar({ job, onCancel, onClose }) {
|
|
967
|
+
const failed = (job.failed || []).length
|
|
968
|
+
const ok = Math.max(0, (job.done || 0) - failed)
|
|
969
|
+
const label = job.state === 'running'
|
|
970
|
+
? `批量补摘要:${job.done || 0}/${job.total || 0}${failed ? `,失败 ${failed}` : ''}`
|
|
971
|
+
: `批量补摘要结束:成功 ${ok}/${job.total || 0}${failed ? `,失败 ${failed}` : ''}${job.state === 'cancelled' ? '(已停止,再点继续)' : ''}`
|
|
972
|
+
return h(
|
|
973
|
+
'div',
|
|
974
|
+
{
|
|
975
|
+
style: {
|
|
976
|
+
position: 'absolute', left: 20, right: 20, bottom: 24, zIndex: 30,
|
|
977
|
+
background: T.bg1, border: `1px solid ${line}`, borderRadius: R.md,
|
|
978
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.28)', padding: '10px 12px',
|
|
979
|
+
display: 'flex', flexDirection: 'column', gap: 6,
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: SP.md } },
|
|
983
|
+
h('div', { style: { flex: '1 1 auto', fontSize: FS.body, fontWeight: 600 } }, label),
|
|
984
|
+
job.state === 'running'
|
|
985
|
+
? h(Btn, { small: true, ghost: true, onClick: onCancel, title: '跑完当前这篇后停下,下次再点继续' }, '停止')
|
|
986
|
+
: h(Btn, { small: true, ghost: true, onClick: onClose }, '收起')),
|
|
987
|
+
h('div', {
|
|
988
|
+
style: {
|
|
989
|
+
fontFamily: mono, fontSize: 11.5, color: muted, lineHeight: 1.5,
|
|
990
|
+
maxHeight: 88, overflowY: 'auto', whiteSpace: 'pre-wrap',
|
|
991
|
+
},
|
|
992
|
+
}, (job.lines || []).slice(-5).join('\n')))
|
|
993
|
+
}
|
|
994
|
+
|
|
565
995
|
function Papers({ papers, loading, onSelect, selected, selectedData, onSummarize, running, vaultName }) {
|
|
566
996
|
const [q, setQ] = useState('')
|
|
567
997
|
const [domain, setDomain] = useState('')
|
|
568
998
|
const [onlyPending, setOnlyPending] = useState(false)
|
|
999
|
+
const [hovered, setHovered] = useState('')
|
|
569
1000
|
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
1001
|
const items = (papers || []).filter((p) => {
|
|
571
1002
|
if (onlyPending && !p.pending) return false
|
|
@@ -573,93 +1004,85 @@ window.__ModuleLoader__.load({
|
|
|
573
1004
|
if (!q) return true
|
|
574
1005
|
return `${p.title} ${p.year} ${p.journal} ${p.tags.join(' ')}`.toLowerCase().includes(q.toLowerCase())
|
|
575
1006
|
})
|
|
576
|
-
const rows = items.map((p) =>
|
|
577
|
-
|
|
1007
|
+
const rows = items.map((p) => {
|
|
1008
|
+
const on = selected === p.dir
|
|
1009
|
+
const dom = domainOfPaper(p)
|
|
1010
|
+
const domColor = domainColor(dom, domains)
|
|
1011
|
+
const meta = p.journal || dom || '—'
|
|
1012
|
+
return h(
|
|
578
1013
|
'div',
|
|
579
1014
|
{
|
|
580
1015
|
key: p.dir,
|
|
581
1016
|
onClick: () => onSelect(p.dir),
|
|
1017
|
+
onMouseEnter: () => setHovered(p.dir),
|
|
1018
|
+
onMouseLeave: () => setHovered(''),
|
|
582
1019
|
title: p.title,
|
|
583
1020
|
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}`,
|
|
1021
|
+
position: 'relative', display: 'grid', gridTemplateColumns: '38px 1fr', gap: 10,
|
|
1022
|
+
padding: '10px 10px 11px', borderRadius: 8, cursor: 'pointer',
|
|
1023
|
+
background: on ? 'rgba(77,107,254,0.12)' : hovered === p.dir ? hoverBg : 'transparent',
|
|
589
1024
|
},
|
|
590
1025
|
},
|
|
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),
|
|
1026
|
+
on ? h('div', { style: { position: 'absolute', left: 0, top: 9, bottom: 9, width: 2, borderRadius: 2, background: ACCENT } }) : null,
|
|
1027
|
+
h('div', { style: { fontFamily: mono, fontSize: 12, color: T.text3, paddingTop: 2, fontVariantNumeric: 'tabular-nums' } }, p.year || '—'),
|
|
1028
|
+
h('div', { style: { minWidth: 0 } },
|
|
1029
|
+
h('div', {
|
|
627
1030
|
style: {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
color: 'inherit', fontSize: FS.caption,
|
|
1031
|
+
fontSize: 13.5, lineHeight: 1.45, fontWeight: 550, display: '-webkit-box',
|
|
1032
|
+
WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
|
631
1033
|
},
|
|
1034
|
+
}, p.title),
|
|
1035
|
+
h('div', { style: { marginTop: 4, display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, fontSize: 12, color: T.caption } },
|
|
1036
|
+
h('span', { style: { flex: '1 1 auto', display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0 } },
|
|
1037
|
+
h('span', { style: { flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 5, color: dom ? domColor : T.caption } },
|
|
1038
|
+
h('span', { style: { width: 5, height: 5, borderRadius: '50%', background: dom ? domColor : 'transparent' } }), dom || '—'),
|
|
1039
|
+
h('span', { style: { flex: '1 1 auto', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, p.journal || '')),
|
|
1040
|
+
p.pending
|
|
1041
|
+
? h('span', { style: { flex: '0 0 auto', color: ORANGE, display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
1042
|
+
h('span', { style: { width: 5, height: 5, borderRadius: '50%', background: 'currentColor' } }), '待总结')
|
|
1043
|
+
: null)))
|
|
1044
|
+
})
|
|
1045
|
+
const left = h(
|
|
1046
|
+
'div',
|
|
1047
|
+
{ style: { minHeight: 0, display: 'flex', flexDirection: 'column', background: T.bg1, borderRight: `1px solid ${line}` } },
|
|
1048
|
+
h(RailHead, { title: '论文', count: fmt((papers || []).length) }),
|
|
1049
|
+
h(RailSearch, { value: q, onChange: setQ, placeholder: '搜索标题 / 年份 / 期刊 / 标签' }),
|
|
1050
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px 10px' } },
|
|
1051
|
+
h('select', {
|
|
1052
|
+
value: domain,
|
|
1053
|
+
onChange: (e) => setDomain(e.target.value),
|
|
1054
|
+
style: {
|
|
1055
|
+
flex: '1 1 auto', minWidth: 0, height: 26, padding: '0 8px', border: 'none',
|
|
1056
|
+
borderRadius: 6, background: T.bg2, color: T.text2, fontSize: 12.5,
|
|
632
1057
|
},
|
|
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} 篇`),
|
|
1058
|
+
}, [h('option', { key: '', value: '' }, '全部领域')].concat(domains.map((d) => h('option', { key: d, value: d }, d)))),
|
|
1059
|
+
h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, color: T.text2, cursor: 'pointer', whiteSpace: 'nowrap', fontSize: 12.5 } },
|
|
1060
|
+
h('input', { type: 'checkbox', checked: onlyPending, onChange: (e) => setOnlyPending(e.target.checked), style: { accentColor: ACCENT, margin: 0 } }),
|
|
1061
|
+
'只看待补')),
|
|
1062
|
+
h('div', { style: { padding: '0 16px 8px', fontSize: 12, color: T.caption, fontVariantNumeric: 'tabular-nums' } }, `显示 ${items.length} / ${(papers || []).length} 篇`),
|
|
643
1063
|
loading
|
|
644
|
-
? h('div', { style: { color: muted, fontSize: FS.body } }, '加载中…')
|
|
645
|
-
: h('div', { style: {
|
|
1064
|
+
? h('div', { style: { color: muted, fontSize: FS.body, padding: '8px 16px' } }, '加载中…')
|
|
1065
|
+
: h('div', { style: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', padding: '0 8px 32px' } }, rows),
|
|
646
1066
|
)
|
|
647
1067
|
const right = h(
|
|
648
1068
|
'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,
|
|
1069
|
+
{ style: { minHeight: 0, overflowY: 'auto' } },
|
|
658
1070
|
selected && selectedData
|
|
659
|
-
? h(
|
|
660
|
-
|
|
1071
|
+
? h(ReaderPane, null,
|
|
1072
|
+
selectedData.pending
|
|
1073
|
+
? h('div', {
|
|
1074
|
+
style: {
|
|
1075
|
+
display: 'flex', alignItems: 'center', gap: SP.md, marginBottom: 20,
|
|
1076
|
+
padding: '10px 14px', borderRadius: 8, background: 'rgba(196,123,30,0.10)', border: '1px solid rgba(196,123,30,0.30)',
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
h('span', { style: { fontSize: FS.body, flex: '1 1 auto' } }, '这篇条目还没有摘要与标签'),
|
|
1080
|
+
h(Btn, { small: true, primary: true, disabled: running, onClick: () => onSummarize(selected) }, running ? '处理中…' : '生成摘要与标签'))
|
|
1081
|
+
: null,
|
|
1082
|
+
h(PaperPreview, { data: selectedData, onOpenPaper: onSelect }))
|
|
1083
|
+
: h(ReaderEmpty, { title: '从左侧选一篇论文', sub: '可搜索、按领域筛选、只看待补;条目内容按摘要排版显示' }),
|
|
661
1084
|
)
|
|
662
|
-
return h('div', { style: { display: 'grid', gridTemplateColumns: '
|
|
1085
|
+
return h('div', { style: { display: 'grid', gridTemplateColumns: '336px 1fr', height: '100%', minHeight: 0 } }, left, right)
|
|
663
1086
|
}
|
|
664
1087
|
|
|
665
1088
|
// obsidian:// 只能由宿主侧拉起:桌面版对自定义协议的 window.open 一律 deny
|
|
@@ -673,9 +1096,10 @@ window.__ModuleLoader__.load({
|
|
|
673
1096
|
return await r.json()
|
|
674
1097
|
}
|
|
675
1098
|
|
|
676
|
-
function PaperPreview({ data,
|
|
1099
|
+
function PaperPreview({ data, onOpenPaper }) {
|
|
677
1100
|
const [msg, setMsg] = useState('')
|
|
678
1101
|
if (!data) return null
|
|
1102
|
+
const fm = parsePaperFrontmatter(data.index)
|
|
679
1103
|
const rel = `1-Papers/${data.dir}/index.md`
|
|
680
1104
|
const copyPath = () => {
|
|
681
1105
|
try {
|
|
@@ -697,61 +1121,37 @@ window.__ModuleLoader__.load({
|
|
|
697
1121
|
setMsg('打开失败:' + String(e) + ',已复制条目路径')
|
|
698
1122
|
}
|
|
699
1123
|
}
|
|
700
|
-
const
|
|
701
|
-
.
|
|
1124
|
+
const meta = [
|
|
1125
|
+
{ label: '年份', value: fm.year },
|
|
1126
|
+
{ label: '期刊/会议', value: fm.journal },
|
|
1127
|
+
{ label: 'DOI', value: fm.doi || (fm.arxiv ? 'arXiv ' + fm.arxiv : '') },
|
|
1128
|
+
{ label: '篇幅', value: [fm.pages && fm.pages + ' 页', fm.figures && fm.figures + ' 图'].filter(Boolean).join(' · ') },
|
|
1129
|
+
{ label: '引用键', value: fm.citekey },
|
|
1130
|
+
]
|
|
1131
|
+
const concepts = data.concepts || []
|
|
1132
|
+
const taxonomy = (data.taxonomy || []).slice().sort((a, b) => {
|
|
1133
|
+
const order = ['领域', '方法', '应用', '仪器']
|
|
1134
|
+
return order.indexOf(tagCategory(a)) - order.indexOf(tagCategory(b))
|
|
1135
|
+
})
|
|
702
1136
|
return h(
|
|
703
1137
|
'div',
|
|
704
1138
|
null,
|
|
705
|
-
h(
|
|
706
|
-
'div',
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
),
|
|
1139
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 16 } },
|
|
1140
|
+
h('div', { style: { flex: '1 1 auto', fontSize: FS.page, lineHeight: 1.4, fontWeight: 650 } }, fm.title || data.dir),
|
|
1141
|
+
h(DocActions, null,
|
|
1142
|
+
h(Btn, { small: true, ghost: true, onClick: copyPath, title: '复制库内相对路径' }, '复制路径'),
|
|
1143
|
+
h(Btn, { small: true, onClick: openInObsidian, title: '用 Obsidian 打开这篇笔记' }, '在 Obsidian 打开'))),
|
|
1144
|
+
h(MetaStrip, { items: meta }),
|
|
712
1145
|
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
|
-
)
|
|
1146
|
+
concepts.length || taxonomy.length
|
|
1147
|
+
? h('div', { style: { marginTop: 16, display: 'flex', flexWrap: 'wrap', gap: 6 } },
|
|
1148
|
+
concepts.map((c) => h(ConceptChip, { key: 'c' + c, name: c })),
|
|
1149
|
+
taxonomy.map((t) => h(TaxChip, { key: 't' + t, id: t })))
|
|
724
1150
|
: 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
|
-
),
|
|
1151
|
+
h('div', { style: { marginTop: 26 } }, h(Markdown, { source: data.index, ledeHeading: '一句话总结', onOpenPaper })),
|
|
745
1152
|
)
|
|
746
1153
|
}
|
|
747
1154
|
|
|
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
1155
|
function layoutGraph(data) {
|
|
756
1156
|
const W = 1000
|
|
757
1157
|
const H = 660
|
|
@@ -883,7 +1283,7 @@ window.__ModuleLoader__.load({
|
|
|
883
1283
|
dots.push(h('span', { key: 'tax', style: { display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: FS.caption, color: muted } },
|
|
884
1284
|
h('span', { style: { width: 8, height: 8, borderRadius: 999, background: '#ffd700' } }), '分类'))
|
|
885
1285
|
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:
|
|
1286
|
+
h('span', { style: { width: 8, height: 8, borderRadius: 999, background: CONCEPT_COLOR } }), '概念'))
|
|
887
1287
|
|
|
888
1288
|
const graph = (data.papers.length + (data.concepts || []).length) === 0
|
|
889
1289
|
? h('div', { style: { height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: faint, fontSize: FS.body } }, '还没有论文或概念,导入几篇后这里会出现关系图')
|
|
@@ -916,7 +1316,7 @@ window.__ModuleLoader__.load({
|
|
|
916
1316
|
? !domain || ((linked && linked.papers) || []).some((d) => visible(paperById.get(d) || { id: d }))
|
|
917
1317
|
: visible(p) || (!focus && !domain)
|
|
918
1318
|
const dim = focusId ? !focusNeighbors.has(n.id) : !vis
|
|
919
|
-
const fill = isConcept ?
|
|
1319
|
+
const fill = isConcept ? CONCEPT_COLOR : isTax ? '#d5a94a' : domainColor(p.domain || '其他', domains)
|
|
920
1320
|
const deg = sim.degree.get(sim.nodes.indexOf(n)) || 0
|
|
921
1321
|
const r = isConcept ? 10 + Math.min(6, ((n.ref.papers || []).length || 0) / 8)
|
|
922
1322
|
: isTax ? 6 + Math.min(5, deg / 3)
|
|
@@ -983,6 +1383,8 @@ window.__ModuleLoader__.load({
|
|
|
983
1383
|
const [active, setActive] = useState('')
|
|
984
1384
|
const [detail, setDetail] = useState(null)
|
|
985
1385
|
const [msg, setMsg] = useState('')
|
|
1386
|
+
const [q, setQ] = useState('')
|
|
1387
|
+
const [hovered, setHovered] = useState('')
|
|
986
1388
|
|
|
987
1389
|
const rel = detail ? `2-Concepts/${detail.name}.md` : ''
|
|
988
1390
|
const openInObsidian = async () => {
|
|
@@ -1031,67 +1433,100 @@ window.__ModuleLoader__.load({
|
|
|
1031
1433
|
}
|
|
1032
1434
|
}
|
|
1033
1435
|
|
|
1436
|
+
const filtered = (items || []).filter((c) => {
|
|
1437
|
+
if (!q) return true
|
|
1438
|
+
const hay = `${c.name} ${c.title} ${(c.aliases || []).join(' ')} ${c.oneLiner || ''}`.toLowerCase()
|
|
1439
|
+
return hay.includes(q.toLowerCase())
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1442
|
+
const rows = filtered.map((c) => {
|
|
1443
|
+
const on = active === c.name
|
|
1444
|
+
return h(
|
|
1445
|
+
'div',
|
|
1446
|
+
{
|
|
1447
|
+
key: c.name,
|
|
1448
|
+
onClick: () => open(c.name),
|
|
1449
|
+
onMouseEnter: () => setHovered(c.name),
|
|
1450
|
+
onMouseLeave: () => setHovered(''),
|
|
1451
|
+
title: c.oneLiner || c.title,
|
|
1452
|
+
style: {
|
|
1453
|
+
position: 'relative', display: 'grid', gridTemplateColumns: '1fr auto', gap: 10,
|
|
1454
|
+
padding: '10px 10px 11px', borderRadius: 8, cursor: 'pointer',
|
|
1455
|
+
background: on ? 'rgba(77,107,254,0.12)' : hovered === c.name ? hoverBg : 'transparent',
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
on ? h('div', { style: { position: 'absolute', left: 0, top: 9, bottom: 9, width: 2, borderRadius: 2, background: CONCEPT_COLOR } }) : null,
|
|
1459
|
+
h('div', { style: { minWidth: 0 } },
|
|
1460
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 } },
|
|
1461
|
+
h('span', { style: { width: 6, height: 6, borderRadius: '50%', background: CONCEPT_COLOR, flex: '0 0 auto' } }),
|
|
1462
|
+
h('div', { style: { fontSize: 14, fontWeight: 550, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.title || c.name)),
|
|
1463
|
+
c.oneLiner
|
|
1464
|
+
? h('div', { style: { marginTop: 3, fontSize: 12, color: T.caption, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', paddingLeft: 13 } }, c.oneLiner)
|
|
1465
|
+
: null),
|
|
1466
|
+
h('div', { style: { fontFamily: mono, fontSize: 12, color: hexA(CONCEPT_COLOR, 0.75), fontVariantNumeric: 'tabular-nums', paddingTop: 3 } }, String(c.papersCount || 0)))
|
|
1467
|
+
})
|
|
1468
|
+
|
|
1034
1469
|
const left = h(
|
|
1035
1470
|
'div',
|
|
1036
|
-
{ style: {
|
|
1471
|
+
{ style: { minHeight: 0, display: 'flex', flexDirection: 'column', background: T.bg1, borderRight: `1px solid ${line}` } },
|
|
1472
|
+
h(RailHead, { title: '概念', count: items ? fmt(items.length) : '—' }),
|
|
1473
|
+
h(RailSearch, { value: q, onChange: setQ, placeholder: '搜索概念 / 别名' }),
|
|
1474
|
+
h('div', { style: { padding: '0 16px 8px', fontSize: 12, color: T.caption } }, '按引用论文数排序'),
|
|
1037
1475
|
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 } }, '加载中…'),
|
|
1476
|
+
? h('div', { style: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', padding: '0 8px 32px' } }, rows)
|
|
1477
|
+
: h('div', { style: { color: muted, fontSize: FS.body, padding: '8px 16px' } }, '加载中…'),
|
|
1065
1478
|
)
|
|
1066
1479
|
|
|
1480
|
+
const fm = detail ? parsePaperFrontmatter(detail.markdown) : {}
|
|
1481
|
+
const tags = (fm.tags || []).filter((t) => !t.startsWith('状态/'))
|
|
1482
|
+
const related = (detail && detail.papers ? detail.papers.slice() : []).sort((a, b) => (parseFloat(b.year || '0') || 0) - (parseFloat(a.year || '0') || 0))
|
|
1067
1483
|
const right = h(
|
|
1068
1484
|
'div',
|
|
1069
|
-
{ style: {
|
|
1485
|
+
{ style: { minHeight: 0, overflowY: 'auto' } },
|
|
1070
1486
|
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
|
-
|
|
1487
|
+
? h(ReaderPane, null,
|
|
1488
|
+
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 16 } },
|
|
1489
|
+
h('div', { style: { flex: '1 1 auto', fontSize: FS.page, lineHeight: 1.4, fontWeight: 650 } }, detail.title || detail.name),
|
|
1490
|
+
h(DocActions, null,
|
|
1491
|
+
h(Btn, { small: true, ghost: true, onClick: copyPath, title: '复制库内相对路径' }, '复制路径'),
|
|
1492
|
+
h(Btn, { small: true, onClick: openInObsidian, title: '用 Obsidian 打开概念页' }, '在 Obsidian 打开'))),
|
|
1493
|
+
h(MetaStrip, {
|
|
1494
|
+
items: [
|
|
1495
|
+
{ label: '类型', value: '概念页' },
|
|
1496
|
+
{ label: '引用', value: (detail.papersCount || 0) + ' 篇论文' },
|
|
1497
|
+
{ label: '别名', value: (fm.aliases || detail.aliases || []).slice(0, 4).join(' · ') },
|
|
1498
|
+
{ label: '更新', value: fm.updated },
|
|
1499
|
+
],
|
|
1500
|
+
}),
|
|
1501
|
+
msg ? h('div', { style: { fontSize: FS.caption, color: ERROR_TEXT, marginTop: SP.xs } }, msg) : null,
|
|
1502
|
+
tags.length
|
|
1503
|
+
? h('div', { style: { marginTop: 16, display: 'flex', flexWrap: 'wrap', gap: 6 } }, tags.map((t) => h(TaxChip, { key: t, id: t })))
|
|
1504
|
+
: null,
|
|
1505
|
+
h('div', { style: { marginTop: 26 } }, h(Markdown, { source: detail.markdown, ledeFirstQuote: true, onOpenPaper })),
|
|
1506
|
+
related.length
|
|
1507
|
+
? h('div', null,
|
|
1508
|
+
h(H2, null, '相关论文'),
|
|
1509
|
+
h('div', null, related.map((p) => h('div', {
|
|
1510
|
+
key: p.dir,
|
|
1511
|
+
onClick: () => onOpenPaper(p.dir),
|
|
1512
|
+
title: p.title,
|
|
1513
|
+
onMouseEnter: () => setHovered('paper:' + p.dir),
|
|
1514
|
+
onMouseLeave: () => setHovered(''),
|
|
1515
|
+
style: {
|
|
1516
|
+
display: 'grid', gridTemplateColumns: '42px 1fr', gap: 12, padding: '9px 8px', borderRadius: 7,
|
|
1517
|
+
cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.045)',
|
|
1518
|
+
background: hovered === 'paper:' + p.dir ? hoverBg : 'transparent',
|
|
1519
|
+
},
|
|
1520
|
+
},
|
|
1521
|
+
h('div', { style: { fontFamily: mono, fontSize: 12, color: T.text3, paddingTop: 2, fontVariantNumeric: 'tabular-nums' } }, p.year || '—'),
|
|
1522
|
+
h('div', { style: { fontSize: 13.5, lineHeight: 1.5 } }, p.title)))))
|
|
1523
|
+
: null)
|
|
1524
|
+
: h(ReaderEmpty, {
|
|
1525
|
+
title: items && items.length ? '从左侧选一个概念' : '还没有概念页',
|
|
1526
|
+
sub: items && items.length ? '概念页是跨论文综合,条目内容按摘要排版显示' : '概念页由 tools/ 下的素材、校验、索引脚本生成,规范见 config/CONCEPT_SPEC.md',
|
|
1527
|
+
}),
|
|
1092
1528
|
)
|
|
1093
|
-
|
|
1094
|
-
return h('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start' } }, left, right)
|
|
1529
|
+
return h('div', { style: { display: 'grid', gridTemplateColumns: '300px 1fr', height: '100%', minHeight: 0 } }, left, right)
|
|
1095
1530
|
}
|
|
1096
1531
|
|
|
1097
1532
|
function PaperDepotView() {
|
|
@@ -1107,6 +1542,8 @@ window.__ModuleLoader__.load({
|
|
|
1107
1542
|
const [setup, setSetup] = useState(null)
|
|
1108
1543
|
const [showSetup, setShowSetup] = useState(false)
|
|
1109
1544
|
const [showImport, setShowImport] = useState(false)
|
|
1545
|
+
const [jobInfo, setJobInfo] = useState(null)
|
|
1546
|
+
|
|
1110
1547
|
|
|
1111
1548
|
const loadSetup = useCallback(async () => {
|
|
1112
1549
|
try {
|
|
@@ -1153,11 +1590,13 @@ window.__ModuleLoader__.load({
|
|
|
1153
1590
|
|
|
1154
1591
|
const startPolling = useCallback(async () => {
|
|
1155
1592
|
setRunning(true)
|
|
1156
|
-
|
|
1593
|
+
// 批量补摘要可能跑几个小时:轮询上限放到 ~3 小时,任务结束后立即退出
|
|
1594
|
+
for (let i = 0; i < 9000; i++) {
|
|
1157
1595
|
await new Promise((r) => setTimeout(r, 1200))
|
|
1158
1596
|
try {
|
|
1159
1597
|
const j = await (await api('/job')).json()
|
|
1160
1598
|
if (j.job) {
|
|
1599
|
+
setJobInfo(j.job)
|
|
1161
1600
|
setLog((j.job.lines || []).slice(-300))
|
|
1162
1601
|
if (j.job.state !== 'running') {
|
|
1163
1602
|
setRunning(false)
|
|
@@ -1255,6 +1694,33 @@ window.__ModuleLoader__.load({
|
|
|
1255
1694
|
}
|
|
1256
1695
|
}, [startPolling, openPaper, setLog])
|
|
1257
1696
|
|
|
1697
|
+
// 批量补摘要:排队把「待生成」的条目跑完;中断后再点会从剩下的继续
|
|
1698
|
+
const onSummarizePending = useCallback(async () => {
|
|
1699
|
+
setJobInfo(null)
|
|
1700
|
+
try {
|
|
1701
|
+
const r = await fetch('/paperdepot/api/summarize-pending', { method: 'POST', credentials: 'same-origin' })
|
|
1702
|
+
const j = await r.json()
|
|
1703
|
+
if (!j.ok) {
|
|
1704
|
+
setLog([`❌ ${j.error || r.status}`])
|
|
1705
|
+
return
|
|
1706
|
+
}
|
|
1707
|
+
setLog([`▶ 批量补摘要:排队 ${j.total} 篇(库里待生成 ${j.pending} 篇)`])
|
|
1708
|
+
await startPolling()
|
|
1709
|
+
} catch (e) {
|
|
1710
|
+
setLog([`❌ ${String(e)}`])
|
|
1711
|
+
}
|
|
1712
|
+
}, [startPolling, setLog])
|
|
1713
|
+
|
|
1714
|
+
const onCancelJob = useCallback(async () => {
|
|
1715
|
+
try {
|
|
1716
|
+
const r = await fetch('/paperdepot/api/job-cancel', { method: 'POST', credentials: 'same-origin' })
|
|
1717
|
+
const j = await r.json()
|
|
1718
|
+
if (!j.ok) setLog((old) => [...old, `❌ ${j.error || r.status}`])
|
|
1719
|
+
} catch (e) {
|
|
1720
|
+
setLog((old) => [...old, `❌ ${String(e)}`])
|
|
1721
|
+
}
|
|
1722
|
+
}, [setLog])
|
|
1723
|
+
|
|
1258
1724
|
const onSetupSaved = useCallback(async () => {
|
|
1259
1725
|
try {
|
|
1260
1726
|
const s = await (await api('/setup/status')).json()
|
|
@@ -1301,11 +1767,11 @@ window.__ModuleLoader__.load({
|
|
|
1301
1767
|
['concepts', '概念', d ? String(d.concepts) : ''],
|
|
1302
1768
|
]
|
|
1303
1769
|
const content = setup && !setup.hasKey
|
|
1304
|
-
? h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px
|
|
1770
|
+
? h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px 72px', boxSizing: 'border-box' } }, h(SetupCard, { setup, onSaved: onSetupSaved }))
|
|
1305
1771
|
: d
|
|
1306
1772
|
? tab === 'papers'
|
|
1307
1773
|
? h(Papers, { papers, loading: papersLoading, onSelect: openPaper, selected, selectedData, onSummarize, running, vaultName })
|
|
1308
|
-
: h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px
|
|
1774
|
+
: h('div', { style: { height: '100%', overflowY: 'auto', padding: '20px 20px 72px', boxSizing: 'border-box' } },
|
|
1309
1775
|
tab === 'overview'
|
|
1310
1776
|
? h(Overview, { d, onOpen: openFromGraph })
|
|
1311
1777
|
: h(Concepts, { onOpenPaper: openFromGraph, vaultName }))
|
|
@@ -1404,13 +1870,31 @@ window.__ModuleLoader__.load({
|
|
|
1404
1870
|
style: { fontSize: 12.5, color: muted, background: T.bg2, padding: '1px 6px', borderRadius: 999, lineHeight: 1.5 },
|
|
1405
1871
|
}, count) : null)),
|
|
1406
1872
|
h('div', { style: { flex: '1 1 auto' } }),
|
|
1873
|
+
d && d.pendingSummary
|
|
1874
|
+
? h(Btn, {
|
|
1875
|
+
small: true,
|
|
1876
|
+
ghost: true,
|
|
1877
|
+
onClick: onSummarizePending,
|
|
1878
|
+
disabled: running,
|
|
1879
|
+
title: '把「待生成」的条目排队跑完;可随时停止,下次再点会跳过已完成的',
|
|
1880
|
+
}, running
|
|
1881
|
+
? `任务中(${jobInfo && jobInfo.kind === 'summarize-batch' ? `${jobInfo.done || 0}/${jobInfo.total || 0}` : '…'})`
|
|
1882
|
+
: `批量补摘要(${d.pendingSummary})`)
|
|
1883
|
+
: null,
|
|
1407
1884
|
h(Btn, { small: true, ghost: true, onClick: onRebuildNav, disabled: running, title: '重建论文导航块、分类页与图颜色分组' }, running ? '重建中…' : '重建导航')),
|
|
1408
1885
|
// 内容
|
|
1409
1886
|
h('div', { style: { flex: '1 1 auto', minHeight: 0, position: 'relative' } }, content),
|
|
1887
|
+
// 底部安全区:flex 末尾的固定占位(等价 HarmonyOS 的 bottom safe area / Blank),
|
|
1888
|
+
// 让上方内容整体上移,不贴窗口下沿、不被任务栏压住;不放滚动区里,避免随内容滚走
|
|
1889
|
+
h('div', { style: { flex: '0 0 auto', height: 24 } }),
|
|
1410
1890
|
showSetup && setup && setup.hasKey
|
|
1411
1891
|
? h('div', { style: { position: 'absolute', top: 58, right: 20, zIndex: 15, width: 540, maxWidth: '92vw', maxHeight: 'calc(100% - 80px)', overflowY: 'auto' } },
|
|
1412
1892
|
h(SetupCard, { setup, onSaved: onSetupSaved }))
|
|
1413
1893
|
: null,
|
|
1894
|
+
// 批量补摘要进度条:任务结束时保留,可收起
|
|
1895
|
+
jobInfo && jobInfo.kind === 'summarize-batch'
|
|
1896
|
+
? h(BatchBar, { job: jobInfo, onCancel: onCancelJob, onClose: () => setJobInfo(null) })
|
|
1897
|
+
: null,
|
|
1414
1898
|
h(ImportDialog, { open: showImport, onClose: () => setShowImport(false), running, log, uploadFiles }),
|
|
1415
1899
|
)
|
|
1416
1900
|
}
|