dsh-plugin-litsearch 0.3.0 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -34
- package/client/client.js +1382 -760
- package/lib/host/export/bibtex.js +1 -4
- package/lib/host/export/common.d.ts +0 -2
- package/lib/host/export/common.js +0 -7
- package/lib/host/export/csv.js +1 -5
- package/lib/host/export/markdown.d.ts +1 -1
- package/lib/host/export/markdown.js +9 -26
- package/lib/host/export/ris.js +1 -4
- package/lib/host/paths.d.ts +1 -1
- package/lib/host/paths.js +1 -1
- package/lib/host/pdf/store.d.ts +5 -3
- package/lib/host/pdf/store.js +50 -17
- package/lib/host/router.js +80 -78
- package/lib/host/tasks/service.d.ts +10 -8
- package/lib/host/tasks/service.js +86 -31
- package/lib/host/tasks/states.d.ts +1 -3
- package/lib/host/tasks/states.js +2 -66
- package/lib/host/tasks/stats.js +3 -0
- package/lib/host/tasks/summary.js +1 -1
- package/lib/host/tools.d.ts +1 -1
- package/lib/host/tools.js +6 -29
- package/lib/search/oa.d.ts +8 -0
- package/lib/search/oa.js +53 -6
- package/lib/search/types.d.ts +18 -9
- package/lib/search/types.js +4 -0
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -41,6 +41,10 @@ const TASK_LANGUAGES = [
|
|
|
41
41
|
"ita",
|
|
42
42
|
"por"
|
|
43
43
|
];
|
|
44
|
+
/** 是否需要引导用户到浏览器手动保存。 */
|
|
45
|
+
function isManualPdfFailure(reason) {
|
|
46
|
+
return reason !== void 0 && reason !== "failed";
|
|
47
|
+
}
|
|
44
48
|
SOURCE_IDS.filter((id) => id !== "googlescholar");
|
|
45
49
|
/** 写端点的自定义请求头(与 JSON content-type 一起构成非简单请求)。 */
|
|
46
50
|
const WRITE_HEADER = "x-dsh-litsearch-write";
|
|
@@ -188,7 +192,7 @@ function probeSource(source, signal) {
|
|
|
188
192
|
signal
|
|
189
193
|
});
|
|
190
194
|
}
|
|
191
|
-
/** 批量写入筛读 /
|
|
195
|
+
/** 批量写入筛读 / 分组。 */
|
|
192
196
|
function annotateTask(id, papers, rev, signal) {
|
|
193
197
|
const body = { papers };
|
|
194
198
|
if (rev !== void 0) body.rev = rev;
|
|
@@ -229,13 +233,13 @@ function downloadPaperPdf(taskId, paperId, signal) {
|
|
|
229
233
|
function syncZotero(taskId, paperIds) {
|
|
230
234
|
return request(taskPath(taskId, "/zotero"), writeInit("POST", { paperIds }));
|
|
231
235
|
}
|
|
232
|
-
/**
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
236
|
+
/** 点击「查看已下载」前确认本地文件仍在(页面加载后可能被删除)。 */
|
|
237
|
+
function recheckPaperPdf(taskId, paperId) {
|
|
238
|
+
return request(taskPath(taskId, `/papers/${encodeURIComponent(paperId)}/recheck`), writeInit("POST", {}));
|
|
239
|
+
}
|
|
240
|
+
/** 在系统文件管理器中定位已下载的 PDF(打开所在文件夹并选中文件)。 */
|
|
241
|
+
function revealLocalFile(path) {
|
|
242
|
+
return request("/files/reveal", writeInit("POST", { path }));
|
|
239
243
|
}
|
|
240
244
|
/** Europe PMC 全文 HTML 地址(用于 iframe)。 */
|
|
241
245
|
function paperFullTextUrl(taskId, paperId) {
|
|
@@ -941,18 +945,30 @@ const dicts = {
|
|
|
941
945
|
"pdf.status.unknown": "未获取全文",
|
|
942
946
|
"pdf.status.available": "有全文直链",
|
|
943
947
|
"pdf.status.downloaded": "已下载",
|
|
948
|
+
"pdf.status.missing": "文件已删除",
|
|
944
949
|
"pdf.status.html-only": "仅 HTML 全文",
|
|
945
950
|
"pdf.status.paywalled": "需订阅",
|
|
946
951
|
"pdf.status.blocked": "被出版商拒绝",
|
|
947
952
|
"pdf.status.failed": "获取失败",
|
|
948
|
-
"pdf.
|
|
953
|
+
"pdf.htmlLoading": "HTML 全文加载中…",
|
|
949
954
|
"pdf.closePreview": "收起",
|
|
950
|
-
"pdf.download": "下载
|
|
955
|
+
"pdf.download": "下载",
|
|
956
|
+
"pdf.downloading": "下载中…",
|
|
957
|
+
"pdf.downloadedAction": "查看已下载",
|
|
958
|
+
"pdf.missingAction": "下载文件已删除点击重新下载",
|
|
959
|
+
"pdf.retry": "重试下载",
|
|
960
|
+
"pdf.downloadHint": "下载 PDF 并保存到:{path}",
|
|
961
|
+
"pdf.downloadedHint": "已保存到:{path}\n点击在右侧面板打开该 PDF",
|
|
962
|
+
"pdf.missingHint": "本地 PDF 已被删除或移走,点击重新下载",
|
|
963
|
+
"pdf.revealInFolder": "在文件系统中打开",
|
|
964
|
+
"pdf.downloadingHint": "正在下载 PDF,请稍候…",
|
|
965
|
+
"pdf.retryHint": "上次下载失败:{message}\n点击重试",
|
|
966
|
+
"pdf.downloadDirUnknown": "工作目录下的 dsh-litsearch 文件夹",
|
|
951
967
|
"pdf.fulltext": "HTML 全文",
|
|
952
968
|
"pdf.zotero": "存入 Zotero",
|
|
953
969
|
"pdf.error": "全文获取失败:{message}",
|
|
954
970
|
"pdf.downloaded": "已保存:{name}",
|
|
955
|
-
"pdf.htmlFallback": "
|
|
971
|
+
"pdf.htmlFallback": "未找到可下载的 PDF,已切换为 HTML 全文;可点击 PDF 链接在浏览器打开。",
|
|
956
972
|
"pdf.zoteroDone": "已存入 Zotero:{n} 篇",
|
|
957
973
|
"pdf.zoteroPartial": "Zotero:成功 {saved} 篇,失败 {failed} 篇;{message}",
|
|
958
974
|
"pdf.zoteroFailed": "Zotero 同步失败:{message}",
|
|
@@ -961,7 +977,11 @@ const dicts = {
|
|
|
961
977
|
"pdf.zoteroFallbackDismiss": "关闭",
|
|
962
978
|
"bulk.count": "已选 {n} 篇",
|
|
963
979
|
"bulk.select": "选择这篇文献",
|
|
964
|
-
"bulk.selectPage": "
|
|
980
|
+
"bulk.selectPage": "本页全选",
|
|
981
|
+
"bulk.selectPageHint": "把当前页的文献加入已选,其他页的选中不受影响",
|
|
982
|
+
"bulk.unselectAll": "取消全选",
|
|
983
|
+
"bulk.unselectAllHint": "清空所有已选文献(含其他页)",
|
|
984
|
+
"bulk.keepHint": "已选文献跨页保留,翻页不会丢失",
|
|
965
985
|
"bulk.clear": "清空",
|
|
966
986
|
"bulk.setScreening": "设置筛读状态",
|
|
967
987
|
"bulk.setGroup": "归入分组",
|
|
@@ -971,7 +991,25 @@ const dicts = {
|
|
|
971
991
|
"bulk.insert": "加入对话",
|
|
972
992
|
"bulk.inserted": "已作为引用插入 {n} 篇(编号列表)",
|
|
973
993
|
"bulk.export": "按选中导出",
|
|
974
|
-
"bulk.downloadDone": "批量下载:成功 {ok} 篇,失败 {failed} 篇",
|
|
994
|
+
"bulk.downloadDone": "批量下载:成功 {ok} 篇,需手动获取 {manual} 篇,失败 {failed} 篇",
|
|
995
|
+
"bulk.revealDir": "打开下载目录",
|
|
996
|
+
"batch.title": "批量下载",
|
|
997
|
+
"batch.desc": "共 {n} 篇;已下载的文献会自动跳过,状态实时更新。",
|
|
998
|
+
"batch.total": "共 {n} 篇",
|
|
999
|
+
"batch.downloaded": "已下载 {n}",
|
|
1000
|
+
"batch.downloading": "下载中 {n}",
|
|
1001
|
+
"batch.pending": "待下载 {n}",
|
|
1002
|
+
"batch.manual": "需手动获取 {n}",
|
|
1003
|
+
"batch.failed": "失败 {n}",
|
|
1004
|
+
"batch.progress": "进度 {done}/{total}",
|
|
1005
|
+
"batch.complete": "下载任务完成",
|
|
1006
|
+
"batch.untitled": "(未获取标题)",
|
|
1007
|
+
"batch.close": "关闭",
|
|
1008
|
+
"batch.status.pending": "未下载",
|
|
1009
|
+
"batch.status.downloading": "下载中…",
|
|
1010
|
+
"batch.status.downloaded": "已下载",
|
|
1011
|
+
"batch.status.manual": "需手动获取",
|
|
1012
|
+
"batch.status.failed": "失败",
|
|
975
1013
|
"fulltext.loading": "统计全文覆盖…",
|
|
976
1014
|
"fulltext.hint": "统计基于全部 {total} 篇,与概览同口径,不受当前筛选影响;点状态可切换清单,列表分页加载。",
|
|
977
1015
|
"fulltext.hintFallback": "统计暂不可用,仍可按状态查看文献清单。",
|
|
@@ -987,15 +1025,6 @@ const dicts = {
|
|
|
987
1025
|
"annotate.noGroup": "未分组",
|
|
988
1026
|
"annotate.createGroup": "+ 新建分组…",
|
|
989
1027
|
"annotate.groupHint": "还没有分组;可在任务的「分组」页新建,选「未分组」可清除分组",
|
|
990
|
-
"annotate.expand": "标注",
|
|
991
|
-
"annotate.collapse": "收起标注",
|
|
992
|
-
"annotate.note": "备注",
|
|
993
|
-
"annotate.notePlaceholder": "为什么读 / 方法要点 / 可否引用",
|
|
994
|
-
"annotate.tags": "标签",
|
|
995
|
-
"annotate.tagsPlaceholder": "逗号分隔",
|
|
996
|
-
"annotate.shortlistOrder": "必读顺序",
|
|
997
|
-
"annotate.save": "保存标注",
|
|
998
|
-
"annotate.shortlistBadge": "必读 #{n}",
|
|
999
1028
|
"entity.title": "实体",
|
|
1000
1029
|
"entity.count": "{n} 个实体",
|
|
1001
1030
|
"entity.hint": "来自 PubTator3,按标准标识符去重",
|
|
@@ -1036,7 +1065,7 @@ const dicts = {
|
|
|
1036
1065
|
"groups.empty": "还没有分组。先建一个「为什么读」的类别,再逐篇归入。",
|
|
1037
1066
|
"groups.namePlaceholder": "如:核心方法",
|
|
1038
1067
|
"groups.descPlaceholder": "如:用于方法对比",
|
|
1039
|
-
"groups.deleteConfirm": "删除分组「{name}
|
|
1068
|
+
"groups.deleteConfirm": "删除分组「{name}」?组内文献会回到未分组,筛读状态不受影响。",
|
|
1040
1069
|
"groups.saved": "分组已保存",
|
|
1041
1070
|
"groups.saving": "正在保存…",
|
|
1042
1071
|
"groups.retry": "重试保存",
|
|
@@ -1044,11 +1073,6 @@ const dicts = {
|
|
|
1044
1073
|
"notes.taskPlaceholder": "记录检索策略、纳入排除标准、阶段性结论…",
|
|
1045
1074
|
"notes.save": "保存笔记",
|
|
1046
1075
|
"notes.saved": "笔记已保存",
|
|
1047
|
-
"notes.paperSection": "逐篇备注与标签",
|
|
1048
|
-
"notes.empty": "还没有逐篇备注。可在结果卡片的「标注」里写。",
|
|
1049
|
-
"notes.loading": "读取逐篇备注…",
|
|
1050
|
-
"notes.hint": "逐篇备注按当前任务已加载的前 200 篇统计。",
|
|
1051
|
-
"notes.tags": "标签:{value}",
|
|
1052
1076
|
"compare.title": "与旧任务对比",
|
|
1053
1077
|
"compare.base": "基准任务",
|
|
1054
1078
|
"compare.pick": "选择一个旧任务",
|
|
@@ -1171,7 +1195,7 @@ const dicts = {
|
|
|
1171
1195
|
"result.infoLabel": "状态与来源",
|
|
1172
1196
|
"result.infoHint": "筛读状态、命中的数据库、被引次数、开放获取与全文状态",
|
|
1173
1197
|
"result.actionLabel": "快捷操作",
|
|
1174
|
-
"result.actionHint": "
|
|
1198
|
+
"result.actionHint": "对这篇文献执行操作:加入对话、打开详情、下载 PDF 或 HTML 全文、存入 Zotero",
|
|
1175
1199
|
"error.noEditor": "当前面板不在会话上下文中,无法插入到输入框",
|
|
1176
1200
|
"error.draftChanged": "输入框草稿已变化,未插入;请避免在插入期间编辑草稿",
|
|
1177
1201
|
"error.sourceFailed": "数据源 {source} 检索失败:{message}",
|
|
@@ -1192,7 +1216,7 @@ const dicts = {
|
|
|
1192
1216
|
"group.zotero": "Zotero",
|
|
1193
1217
|
"group.zoteroHint": "通过 Zotero 桌面端 connector 保存条目与 PDF;只连接本机 127.0.0.1。",
|
|
1194
1218
|
"field.contactEmail": "联系邮箱",
|
|
1195
|
-
"field.contactEmailHint": "作为 OpenAlex / Crossref / NCBI 的 polite pool
|
|
1219
|
+
"field.contactEmailHint": "作为 OpenAlex / Crossref / NCBI 的 polite pool 标识;填写后还会启用 Unpaywall,按 DOI 补全开放获取 PDF 地址,提升批量下载成功率",
|
|
1196
1220
|
"field.ncbiApiKey": "NCBI API Key",
|
|
1197
1221
|
"field.ncbiApiKeyHint": "可选;配置后 PubMed E-utilities 额度从 3 次/秒提升到 10 次/秒",
|
|
1198
1222
|
"field.s2ApiKey": "Semantic Scholar Key",
|
|
@@ -1472,18 +1496,30 @@ const dicts = {
|
|
|
1472
1496
|
"pdf.status.unknown": "No full text yet",
|
|
1473
1497
|
"pdf.status.available": "Full-text link",
|
|
1474
1498
|
"pdf.status.downloaded": "Downloaded",
|
|
1499
|
+
"pdf.status.missing": "File deleted",
|
|
1475
1500
|
"pdf.status.html-only": "HTML full text",
|
|
1476
1501
|
"pdf.status.paywalled": "Paywalled",
|
|
1477
1502
|
"pdf.status.blocked": "Blocked by publisher",
|
|
1478
1503
|
"pdf.status.failed": "Fetch failed",
|
|
1479
|
-
"pdf.
|
|
1504
|
+
"pdf.htmlLoading": "Loading full text…",
|
|
1480
1505
|
"pdf.closePreview": "Close",
|
|
1481
|
-
"pdf.download": "Download
|
|
1506
|
+
"pdf.download": "Download",
|
|
1507
|
+
"pdf.downloading": "Downloading…",
|
|
1508
|
+
"pdf.downloadedAction": "View downloaded",
|
|
1509
|
+
"pdf.missingAction": "Downloaded file deleted — click to re-download",
|
|
1510
|
+
"pdf.retry": "Retry download",
|
|
1511
|
+
"pdf.downloadHint": "Download the PDF to: {path}",
|
|
1512
|
+
"pdf.downloadedHint": "Saved to: {path}\nClick to open the PDF in the right panel",
|
|
1513
|
+
"pdf.missingHint": "The local PDF was deleted or moved. Click to download again.",
|
|
1514
|
+
"pdf.revealInFolder": "Show in file manager",
|
|
1515
|
+
"pdf.downloadingHint": "Downloading PDF, please wait…",
|
|
1516
|
+
"pdf.retryHint": "Last download failed: {message}\nClick to retry",
|
|
1517
|
+
"pdf.downloadDirUnknown": "the dsh-litsearch folder under the workspace",
|
|
1482
1518
|
"pdf.fulltext": "HTML full text",
|
|
1483
1519
|
"pdf.zotero": "Save to Zotero",
|
|
1484
1520
|
"pdf.error": "Full text failed: {message}",
|
|
1485
1521
|
"pdf.downloaded": "Saved: {name}",
|
|
1486
|
-
"pdf.htmlFallback": "No downloadable PDF;
|
|
1522
|
+
"pdf.htmlFallback": "No downloadable PDF found; switched to HTML full text. Open the PDF link in a browser if needed.",
|
|
1487
1523
|
"pdf.zoteroDone": "Saved to Zotero: {n}",
|
|
1488
1524
|
"pdf.zoteroPartial": "Zotero: {saved} saved, {failed} failed; {message}",
|
|
1489
1525
|
"pdf.zoteroFailed": "Zotero sync failed: {message}",
|
|
@@ -1492,7 +1528,11 @@ const dicts = {
|
|
|
1492
1528
|
"pdf.zoteroFallbackDismiss": "Dismiss",
|
|
1493
1529
|
"bulk.count": "{n} selected",
|
|
1494
1530
|
"bulk.select": "Select this paper",
|
|
1495
|
-
"bulk.selectPage": "Select page",
|
|
1531
|
+
"bulk.selectPage": "Select this page",
|
|
1532
|
+
"bulk.selectPageHint": "Add this page to the selection; other pages stay selected",
|
|
1533
|
+
"bulk.unselectAll": "Clear all",
|
|
1534
|
+
"bulk.unselectAllHint": "Clear the whole selection, including other pages",
|
|
1535
|
+
"bulk.keepHint": "Selection is kept when you change pages",
|
|
1496
1536
|
"bulk.clear": "Clear",
|
|
1497
1537
|
"bulk.setScreening": "Set screening",
|
|
1498
1538
|
"bulk.setGroup": "Assign group",
|
|
@@ -1502,7 +1542,25 @@ const dicts = {
|
|
|
1502
1542
|
"bulk.insert": "Add to chat",
|
|
1503
1543
|
"bulk.inserted": "Inserted {n} papers as a quoted numbered list",
|
|
1504
1544
|
"bulk.export": "Export selected",
|
|
1505
|
-
"bulk.downloadDone": "Download: {ok} succeeded, {failed} failed",
|
|
1545
|
+
"bulk.downloadDone": "Download: {ok} succeeded, {manual} need manual save, {failed} failed",
|
|
1546
|
+
"bulk.revealDir": "Open download folder",
|
|
1547
|
+
"batch.title": "Batch download",
|
|
1548
|
+
"batch.desc": "Downloading {n} papers; already downloaded ones are skipped. Status updates live.",
|
|
1549
|
+
"batch.total": "{n} total",
|
|
1550
|
+
"batch.downloaded": "{n} downloaded",
|
|
1551
|
+
"batch.downloading": "{n} downloading",
|
|
1552
|
+
"batch.pending": "{n} pending",
|
|
1553
|
+
"batch.manual": "{n} need manual save",
|
|
1554
|
+
"batch.failed": "{n} failed",
|
|
1555
|
+
"batch.progress": "Progress {done}/{total}",
|
|
1556
|
+
"batch.complete": "Download task complete",
|
|
1557
|
+
"batch.untitled": "(title unavailable)",
|
|
1558
|
+
"batch.close": "Close",
|
|
1559
|
+
"batch.status.pending": "Not downloaded",
|
|
1560
|
+
"batch.status.downloading": "Downloading…",
|
|
1561
|
+
"batch.status.downloaded": "Downloaded",
|
|
1562
|
+
"batch.status.manual": "Manual save",
|
|
1563
|
+
"batch.status.failed": "Failed",
|
|
1506
1564
|
"fulltext.loading": "Computing full-text coverage…",
|
|
1507
1565
|
"fulltext.hint": "Coverage covers all {total} papers (same basis as the overview) and ignores current filters. Click a status to switch lists; the list loads in pages.",
|
|
1508
1566
|
"fulltext.hintFallback": "Coverage is unavailable; you can still browse papers by status.",
|
|
@@ -1518,15 +1576,6 @@ const dicts = {
|
|
|
1518
1576
|
"annotate.noGroup": "Ungrouped",
|
|
1519
1577
|
"annotate.createGroup": "+ New group…",
|
|
1520
1578
|
"annotate.groupHint": "No groups yet; create one on the Groups tab, or pick Ungrouped to clear",
|
|
1521
|
-
"annotate.expand": "Annotate",
|
|
1522
|
-
"annotate.collapse": "Hide annotation",
|
|
1523
|
-
"annotate.note": "Note",
|
|
1524
|
-
"annotate.notePlaceholder": "Why read it / method / quotable",
|
|
1525
|
-
"annotate.tags": "Tags",
|
|
1526
|
-
"annotate.tagsPlaceholder": "comma separated",
|
|
1527
|
-
"annotate.shortlistOrder": "Shortlist order",
|
|
1528
|
-
"annotate.save": "Save annotation",
|
|
1529
|
-
"annotate.shortlistBadge": "Shortlist #{n}",
|
|
1530
1579
|
"entity.title": "Entities",
|
|
1531
1580
|
"entity.count": "{n} entities",
|
|
1532
1581
|
"entity.hint": "From PubTator3, deduped by standard identifier",
|
|
@@ -1575,11 +1624,6 @@ const dicts = {
|
|
|
1575
1624
|
"notes.taskPlaceholder": "Search strategy, inclusion criteria, interim conclusions…",
|
|
1576
1625
|
"notes.save": "Save notes",
|
|
1577
1626
|
"notes.saved": "Notes saved",
|
|
1578
|
-
"notes.paperSection": "Per-paper notes and tags",
|
|
1579
|
-
"notes.empty": "No per-paper notes yet. Use \"Annotate\" on a result card.",
|
|
1580
|
-
"notes.loading": "Reading per-paper notes…",
|
|
1581
|
-
"notes.hint": "Per-paper notes cover the first 200 loaded papers of this task.",
|
|
1582
|
-
"notes.tags": "Tags: {value}",
|
|
1583
1627
|
"compare.title": "Compare with an older task",
|
|
1584
1628
|
"compare.base": "Base task",
|
|
1585
1629
|
"compare.pick": "Pick an older task",
|
|
@@ -1702,7 +1746,7 @@ const dicts = {
|
|
|
1702
1746
|
"result.infoLabel": "Status & sources",
|
|
1703
1747
|
"result.infoHint": "Screening status, matched databases, citations, open access and full-text status",
|
|
1704
1748
|
"result.actionLabel": "Actions",
|
|
1705
|
-
"result.actionHint": "Actions on this paper: insert into chat, open details,
|
|
1749
|
+
"result.actionHint": "Actions on this paper: insert into chat, open details, download PDF or HTML full text, save to Zotero",
|
|
1706
1750
|
"error.noEditor": "This panel is not in a conversation context; cannot insert into the composer",
|
|
1707
1751
|
"error.draftChanged": "The draft changed; nothing was inserted. Avoid editing while inserting.",
|
|
1708
1752
|
"error.sourceFailed": "Source {source} failed: {message}",
|
|
@@ -1723,7 +1767,7 @@ const dicts = {
|
|
|
1723
1767
|
"group.zotero": "Zotero",
|
|
1724
1768
|
"group.zoteroHint": "Saves items and PDFs through the Zotero desktop connector; only 127.0.0.1 is allowed.",
|
|
1725
1769
|
"field.contactEmail": "Contact email",
|
|
1726
|
-
"field.contactEmailHint": "
|
|
1770
|
+
"field.contactEmailHint": "Polite-pool identifier for OpenAlex / Crossref / NCBI; also enables Unpaywall, which adds open-access PDF links by DOI and improves batch downloads",
|
|
1727
1771
|
"field.ncbiApiKey": "NCBI API key",
|
|
1728
1772
|
"field.ncbiApiKeyHint": "Optional; raises PubMed E-utilities quota from 3 req/s to 10 req/s",
|
|
1729
1773
|
"field.s2ApiKey": "Semantic Scholar key",
|
|
@@ -1992,11 +2036,32 @@ const actionRowStyle = {
|
|
|
1992
2036
|
gap: 10,
|
|
1993
2037
|
marginTop: 8
|
|
1994
2038
|
};
|
|
1995
|
-
/**
|
|
2039
|
+
/** 提醒浮层容器:贴面板底部堆叠,不遮挡操作按钮的点击。 */
|
|
2040
|
+
const toastStackStyle = {
|
|
2041
|
+
position: "absolute",
|
|
2042
|
+
left: 15,
|
|
2043
|
+
right: 15,
|
|
2044
|
+
bottom: 14,
|
|
2045
|
+
zIndex: 30,
|
|
2046
|
+
display: "flex",
|
|
2047
|
+
flexDirection: "column",
|
|
2048
|
+
gap: 6,
|
|
2049
|
+
pointerEvents: "none"
|
|
2050
|
+
};
|
|
2051
|
+
/** 底部自动消失的提醒:黑底白字。 */
|
|
1996
2052
|
const toastStyle = {
|
|
2053
|
+
padding: "8px 12px",
|
|
2054
|
+
borderRadius: 8,
|
|
2055
|
+
border: "1px solid rgba(255,255,255,0.16)",
|
|
2056
|
+
background: "rgba(0,0,0,0.92)",
|
|
2057
|
+
color: "#fff",
|
|
1997
2058
|
fontSize: 12,
|
|
1998
|
-
|
|
2059
|
+
lineHeight: 1.5,
|
|
2060
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.28)",
|
|
2061
|
+
pointerEvents: "none"
|
|
1999
2062
|
};
|
|
2063
|
+
/** 下载失败等错误提醒:同样黑底白字,左侧红条区分。 */
|
|
2064
|
+
const toastErrorStyle = { borderLeft: `3px solid ${ERROR}` };
|
|
2000
2065
|
// src/client/TaskListDialog.tsx
|
|
2001
2066
|
/**
|
|
2002
2067
|
* 右上角「搜索任务列表」弹窗:列出全部任务,可打开、重命名、保留、删除。
|
|
@@ -2655,7 +2720,10 @@ function LitsearchPage({ t, onOpenTask }) {
|
|
|
2655
2720
|
};
|
|
2656
2721
|
const pageSize = health?.settings.defaultPageSize ?? 20;
|
|
2657
2722
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2658
|
-
style:
|
|
2723
|
+
style: {
|
|
2724
|
+
...panelStyle,
|
|
2725
|
+
position: "relative"
|
|
2726
|
+
},
|
|
2659
2727
|
children: [
|
|
2660
2728
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2661
2729
|
style: pageHeaderStyle,
|
|
@@ -2747,15 +2815,20 @@ function LitsearchPage({ t, onOpenTask }) {
|
|
|
2747
2815
|
},
|
|
2748
2816
|
children: error
|
|
2749
2817
|
}),
|
|
2750
|
-
pending.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
2751
|
-
style:
|
|
2752
|
-
children:
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2818
|
+
pending.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2819
|
+
style: toastStackStyle,
|
|
2820
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2821
|
+
style: bannerStyle,
|
|
2822
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2823
|
+
style: { minWidth: 0 },
|
|
2824
|
+
children: tpl(t("pending.title"), { n: pending.length })
|
|
2825
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2826
|
+
type: "button",
|
|
2827
|
+
style: bannerButtonStyle,
|
|
2828
|
+
onClick: openPending,
|
|
2829
|
+
children: t("pending.open")
|
|
2830
|
+
})]
|
|
2831
|
+
})
|
|
2759
2832
|
}),
|
|
2760
2833
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
2761
2834
|
style: sectionTitleStyle,
|
|
@@ -2884,14 +2957,18 @@ const historyMetaStyle = {
|
|
|
2884
2957
|
opacity: .66
|
|
2885
2958
|
};
|
|
2886
2959
|
const bannerStyle = {
|
|
2960
|
+
...toastStyle,
|
|
2961
|
+
pointerEvents: "auto",
|
|
2887
2962
|
display: "flex",
|
|
2888
2963
|
alignItems: "center",
|
|
2889
2964
|
justifyContent: "space-between",
|
|
2890
|
-
gap: 8
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2965
|
+
gap: 8
|
|
2966
|
+
};
|
|
2967
|
+
const bannerButtonStyle = {
|
|
2968
|
+
...linkButtonStyle,
|
|
2969
|
+
color: "#fff",
|
|
2970
|
+
textDecoration: "underline",
|
|
2971
|
+
flexShrink: 0
|
|
2895
2972
|
};
|
|
2896
2973
|
// src/client/compose.ts
|
|
2897
2974
|
/** 插入时摘要最多保留多少字符,避免草稿过长。 */
|
|
@@ -2986,6 +3063,133 @@ function taskConditionsText(t, detail) {
|
|
|
2986
3063
|
function truncate(value, max) {
|
|
2987
3064
|
return value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
2988
3065
|
}
|
|
3066
|
+
// src/client/DownloadButton.tsx
|
|
3067
|
+
/**
|
|
3068
|
+
* 「下载 PDF / 查看已下载」动作按钮。
|
|
3069
|
+
*
|
|
3070
|
+
* 未下载时是单个下载按钮;已下载时变为「查看已下载 ▾」拆分按钮:点文字在右侧
|
|
3071
|
+
* 预览本地 PDF,点箭头展开菜单,可在系统文件管理器中定位该文件。文件被删除后
|
|
3072
|
+
* 回到普通按钮并提示点击重新下载。
|
|
3073
|
+
*/
|
|
3074
|
+
/** 已下载状态的绿色,与徽章 / 状态色一致。 */
|
|
3075
|
+
const DOWNLOADED_COLOR = "#2e9e5b";
|
|
3076
|
+
/** 本地文件已被删除的警示色。 */
|
|
3077
|
+
const MISSING_COLOR = "#d64545";
|
|
3078
|
+
function DownloadButton({ t, label, hint, downloadedPath, missing = false, downloading = false, disabled = false, style, onDownload, onOpen, onReveal }) {
|
|
3079
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
3080
|
+
const groupRef = (0, react.useRef)(null);
|
|
3081
|
+
const path = downloadedPath ?? "";
|
|
3082
|
+
const downloaded = !missing && path !== "" && onOpen !== void 0;
|
|
3083
|
+
const color = missing ? MISSING_COLOR : downloaded ? DOWNLOADED_COLOR : void 0;
|
|
3084
|
+
(0, react.useEffect)(() => {
|
|
3085
|
+
if (!menuOpen) return;
|
|
3086
|
+
const closeOnOutside = (event) => {
|
|
3087
|
+
if (groupRef.current !== null && !groupRef.current.contains(event.target)) setMenuOpen(false);
|
|
3088
|
+
};
|
|
3089
|
+
const closeOnEscape = (event) => {
|
|
3090
|
+
if (event.key === "Escape") setMenuOpen(false);
|
|
3091
|
+
};
|
|
3092
|
+
document.addEventListener("mousedown", closeOnOutside);
|
|
3093
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
3094
|
+
return () => {
|
|
3095
|
+
document.removeEventListener("mousedown", closeOnOutside);
|
|
3096
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
3097
|
+
};
|
|
3098
|
+
}, [menuOpen]);
|
|
3099
|
+
const main = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3100
|
+
type: "button",
|
|
3101
|
+
style: {
|
|
3102
|
+
...style,
|
|
3103
|
+
display: "inline-flex",
|
|
3104
|
+
alignItems: "center",
|
|
3105
|
+
gap: 4,
|
|
3106
|
+
...color === void 0 ? null : { color }
|
|
3107
|
+
},
|
|
3108
|
+
disabled: disabled || downloading,
|
|
3109
|
+
title: hint,
|
|
3110
|
+
"aria-busy": downloading,
|
|
3111
|
+
onClick: () => {
|
|
3112
|
+
if (downloaded) onOpen?.(path);
|
|
3113
|
+
else onDownload();
|
|
3114
|
+
},
|
|
3115
|
+
children: [downloading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpinnerIcon, {}), label]
|
|
3116
|
+
});
|
|
3117
|
+
if (!downloaded || onReveal === void 0) return main;
|
|
3118
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3119
|
+
ref: groupRef,
|
|
3120
|
+
style: GROUP_STYLE$2,
|
|
3121
|
+
children: [
|
|
3122
|
+
main,
|
|
3123
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3124
|
+
type: "button",
|
|
3125
|
+
style: {
|
|
3126
|
+
...style,
|
|
3127
|
+
...ARROW_STYLE,
|
|
3128
|
+
color: DOWNLOADED_COLOR
|
|
3129
|
+
},
|
|
3130
|
+
"aria-haspopup": "menu",
|
|
3131
|
+
"aria-expanded": menuOpen,
|
|
3132
|
+
"aria-label": t("pdf.revealInFolder"),
|
|
3133
|
+
title: t("pdf.revealInFolder"),
|
|
3134
|
+
disabled,
|
|
3135
|
+
onClick: () => setMenuOpen((value) => !value),
|
|
3136
|
+
children: "▾"
|
|
3137
|
+
}),
|
|
3138
|
+
menuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3139
|
+
role: "menu",
|
|
3140
|
+
style: MENU_STYLE,
|
|
3141
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3142
|
+
type: "button",
|
|
3143
|
+
role: "menuitem",
|
|
3144
|
+
style: MENU_ITEM_STYLE,
|
|
3145
|
+
onClick: () => {
|
|
3146
|
+
setMenuOpen(false);
|
|
3147
|
+
onReveal?.(path);
|
|
3148
|
+
},
|
|
3149
|
+
children: t("pdf.revealInFolder")
|
|
3150
|
+
})
|
|
3151
|
+
})
|
|
3152
|
+
]
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
const GROUP_STYLE$2 = {
|
|
3156
|
+
position: "relative",
|
|
3157
|
+
display: "inline-flex",
|
|
3158
|
+
alignItems: "center",
|
|
3159
|
+
gap: 2
|
|
3160
|
+
};
|
|
3161
|
+
const ARROW_STYLE = {
|
|
3162
|
+
padding: "0 1px",
|
|
3163
|
+
lineHeight: 1,
|
|
3164
|
+
fontSize: 20
|
|
3165
|
+
};
|
|
3166
|
+
const MENU_STYLE = {
|
|
3167
|
+
position: "absolute",
|
|
3168
|
+
top: "100%",
|
|
3169
|
+
right: 0,
|
|
3170
|
+
marginTop: 3,
|
|
3171
|
+
zIndex: 40,
|
|
3172
|
+
display: "flex",
|
|
3173
|
+
flexDirection: "column",
|
|
3174
|
+
minWidth: 132,
|
|
3175
|
+
padding: 2,
|
|
3176
|
+
borderRadius: 6,
|
|
3177
|
+
border: "1px solid rgba(127,127,127,0.3)",
|
|
3178
|
+
background: "#fff",
|
|
3179
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.18)"
|
|
3180
|
+
};
|
|
3181
|
+
const MENU_ITEM_STYLE = {
|
|
3182
|
+
padding: "5px 8px",
|
|
3183
|
+
border: "none",
|
|
3184
|
+
borderRadius: 4,
|
|
3185
|
+
background: "transparent",
|
|
3186
|
+
color: "#1f2328",
|
|
3187
|
+
fontSize: 12,
|
|
3188
|
+
lineHeight: 1.4,
|
|
3189
|
+
textAlign: "left",
|
|
3190
|
+
whiteSpace: "nowrap",
|
|
3191
|
+
cursor: "pointer"
|
|
3192
|
+
};
|
|
2989
3193
|
// src/client/Entities.tsx
|
|
2990
3194
|
/**
|
|
2991
3195
|
* PubTator3 实体标注面板。
|
|
@@ -3059,7 +3263,7 @@ function EntityPanel({ t, taskId, paperId, pmid }) {
|
|
|
3059
3263
|
});
|
|
3060
3264
|
const groups = groupByType(entities);
|
|
3061
3265
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3062
|
-
style: WRAP_STYLE$
|
|
3266
|
+
style: WRAP_STYLE$7,
|
|
3063
3267
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3064
3268
|
style: HINT_STYLE$2,
|
|
3065
3269
|
children: [
|
|
@@ -3104,7 +3308,7 @@ function entityTitle(entity) {
|
|
|
3104
3308
|
entity.identifier
|
|
3105
3309
|
].filter((part) => part !== "").join(" · ");
|
|
3106
3310
|
}
|
|
3107
|
-
const WRAP_STYLE$
|
|
3311
|
+
const WRAP_STYLE$7 = {
|
|
3108
3312
|
display: "flex",
|
|
3109
3313
|
flexDirection: "column",
|
|
3110
3314
|
gap: 5,
|
|
@@ -3159,11 +3363,141 @@ function insertQuote(actions, text, summary) {
|
|
|
3159
3363
|
const span = actions.captureInsertion();
|
|
3160
3364
|
return actions.insertText(text, span) ? "inserted" : "draft-changed";
|
|
3161
3365
|
}
|
|
3366
|
+
// src/client/PreviewFrame.tsx
|
|
3367
|
+
/**
|
|
3368
|
+
* 内联 HTML 全文预览框。
|
|
3369
|
+
*
|
|
3370
|
+
* 打开后先在框内显示旋转图标与醒目的加载文案,内容就绪后再显示 iframe。
|
|
3371
|
+
*/
|
|
3372
|
+
function PreviewFrame({ t, src, height, onClose }) {
|
|
3373
|
+
const [loadedSrc, setLoadedSrc] = (0, react.useState)("");
|
|
3374
|
+
const loaded = loadedSrc === src;
|
|
3375
|
+
const title = t("pdf.fulltext");
|
|
3376
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3377
|
+
style: WRAP_STYLE$6,
|
|
3378
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3379
|
+
style: HEAD_STYLE$3,
|
|
3380
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3381
|
+
type: "button",
|
|
3382
|
+
style: CLOSE_STYLE,
|
|
3383
|
+
onClick: onClose,
|
|
3384
|
+
children: t("pdf.closePreview")
|
|
3385
|
+
})]
|
|
3386
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3387
|
+
style: {
|
|
3388
|
+
...BODY_STYLE$1,
|
|
3389
|
+
height
|
|
3390
|
+
},
|
|
3391
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
3392
|
+
title,
|
|
3393
|
+
src,
|
|
3394
|
+
onLoad: () => setLoadedSrc(src),
|
|
3395
|
+
style: {
|
|
3396
|
+
...IFRAME_STYLE,
|
|
3397
|
+
opacity: loaded ? 1 : 0
|
|
3398
|
+
}
|
|
3399
|
+
}, src), !loaded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3400
|
+
style: OVERLAY_STYLE,
|
|
3401
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3402
|
+
style: LOADING_TEXT_STYLE,
|
|
3403
|
+
children: t("pdf.htmlLoading")
|
|
3404
|
+
})]
|
|
3405
|
+
})]
|
|
3406
|
+
})]
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
/** 不依赖 CSS keyframes 的旋转图标(SMIL),避免注入 <style> 受 CSP 影响。 */
|
|
3410
|
+
function Spinner() {
|
|
3411
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3412
|
+
width: 30,
|
|
3413
|
+
height: 30,
|
|
3414
|
+
viewBox: "0 0 24 24",
|
|
3415
|
+
"aria-hidden": "true",
|
|
3416
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3417
|
+
cx: "12",
|
|
3418
|
+
cy: "12",
|
|
3419
|
+
r: "9",
|
|
3420
|
+
fill: "none",
|
|
3421
|
+
stroke: "rgba(80,140,255,0.22)",
|
|
3422
|
+
strokeWidth: "3"
|
|
3423
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3424
|
+
cx: "12",
|
|
3425
|
+
cy: "12",
|
|
3426
|
+
r: "9",
|
|
3427
|
+
fill: "none",
|
|
3428
|
+
stroke: ACCENT,
|
|
3429
|
+
strokeWidth: "3",
|
|
3430
|
+
strokeLinecap: "round",
|
|
3431
|
+
strokeDasharray: "56",
|
|
3432
|
+
strokeDashoffset: "42",
|
|
3433
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("animateTransform", {
|
|
3434
|
+
attributeName: "transform",
|
|
3435
|
+
type: "rotate",
|
|
3436
|
+
from: "0 12 12",
|
|
3437
|
+
to: "360 12 12",
|
|
3438
|
+
dur: "0.9s",
|
|
3439
|
+
repeatCount: "indefinite"
|
|
3440
|
+
})
|
|
3441
|
+
})]
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
const WRAP_STYLE$6 = {
|
|
3445
|
+
marginTop: 6,
|
|
3446
|
+
border: "1px solid rgba(127,127,127,0.22)",
|
|
3447
|
+
borderRadius: 6,
|
|
3448
|
+
overflow: "hidden"
|
|
3449
|
+
};
|
|
3450
|
+
const HEAD_STYLE$3 = {
|
|
3451
|
+
display: "flex",
|
|
3452
|
+
alignItems: "center",
|
|
3453
|
+
justifyContent: "space-between",
|
|
3454
|
+
gap: 8,
|
|
3455
|
+
padding: "3px 8px",
|
|
3456
|
+
fontSize: 11,
|
|
3457
|
+
background: "rgba(127,127,127,0.08)"
|
|
3458
|
+
};
|
|
3459
|
+
const CLOSE_STYLE = {
|
|
3460
|
+
padding: 0,
|
|
3461
|
+
border: "none",
|
|
3462
|
+
background: "transparent",
|
|
3463
|
+
color: ACCENT,
|
|
3464
|
+
fontSize: 12.5,
|
|
3465
|
+
cursor: "pointer"
|
|
3466
|
+
};
|
|
3467
|
+
const BODY_STYLE$1 = {
|
|
3468
|
+
position: "relative",
|
|
3469
|
+
background: "#fff",
|
|
3470
|
+
overflow: "hidden"
|
|
3471
|
+
};
|
|
3472
|
+
const IFRAME_STYLE = {
|
|
3473
|
+
display: "block",
|
|
3474
|
+
width: "100%",
|
|
3475
|
+
height: "100%",
|
|
3476
|
+
border: "none",
|
|
3477
|
+
background: "#fff",
|
|
3478
|
+
transition: "opacity 0.15s ease"
|
|
3479
|
+
};
|
|
3480
|
+
const OVERLAY_STYLE = {
|
|
3481
|
+
position: "absolute",
|
|
3482
|
+
inset: 0,
|
|
3483
|
+
display: "flex",
|
|
3484
|
+
flexDirection: "column",
|
|
3485
|
+
alignItems: "center",
|
|
3486
|
+
justifyContent: "center",
|
|
3487
|
+
gap: 10,
|
|
3488
|
+
background: "#fff"
|
|
3489
|
+
};
|
|
3490
|
+
const LOADING_TEXT_STYLE = {
|
|
3491
|
+
fontSize: 13.5,
|
|
3492
|
+
fontWeight: 700,
|
|
3493
|
+
letterSpacing: "0.02em",
|
|
3494
|
+
color: ACCENT
|
|
3495
|
+
};
|
|
3162
3496
|
// src/client/PaperPane.tsx
|
|
3163
3497
|
/**
|
|
3164
3498
|
* 独立论文 tab:`dsh-resource://litsearch-paper/<taskId>/<paperId>`。
|
|
3165
3499
|
*
|
|
3166
|
-
*
|
|
3500
|
+
* 展示完整摘要、标识符、筛读与分组、全文预览和 PubTator 实体;数据来自
|
|
3167
3501
|
* 单篇详情端点,避免为了打开一篇论文拉取整页结果。
|
|
3168
3502
|
*/
|
|
3169
3503
|
const PAPER_ADDRESS_PREFIX = "dsh-resource://litsearch-paper/";
|
|
@@ -3194,6 +3528,7 @@ const PDF_COLOR$1 = {
|
|
|
3194
3528
|
unknown: "rgba(127,127,127,0.8)",
|
|
3195
3529
|
available: "#508cff",
|
|
3196
3530
|
downloaded: "#2e9e5b",
|
|
3531
|
+
missing: "#d64545",
|
|
3197
3532
|
"html-only": "#b26a00",
|
|
3198
3533
|
paywalled: "#b26a00",
|
|
3199
3534
|
blocked: "#d64545",
|
|
@@ -3202,20 +3537,22 @@ const PDF_COLOR$1 = {
|
|
|
3202
3537
|
function messageOf$3(error) {
|
|
3203
3538
|
return error instanceof Error ? error.message : String(error);
|
|
3204
3539
|
}
|
|
3205
|
-
function PaperPane({ t, useTabInfo, inputActions }) {
|
|
3540
|
+
function PaperPane({ t, useTabInfo, inputActions, onOpenLocal, onRevealLocal }) {
|
|
3206
3541
|
const info = useTabInfo();
|
|
3207
3542
|
const ref = paperRefFromAddress(info.tab.navigation?.address || info.tab.contentId || "");
|
|
3208
3543
|
const [detail, setDetail] = (0, react.useState)(null);
|
|
3209
3544
|
const [loading, setLoading] = (0, react.useState)(true);
|
|
3210
3545
|
const [error, setError] = (0, react.useState)(null);
|
|
3211
3546
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
3547
|
+
const [downloading, setDownloading] = (0, react.useState)(false);
|
|
3212
3548
|
const [nonce, setNonce] = (0, react.useState)(0);
|
|
3213
|
-
const [toast,
|
|
3214
|
-
const [
|
|
3215
|
-
const
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3549
|
+
const [toast, setToastState] = (0, react.useState)("");
|
|
3550
|
+
const [toastTone, setToastTone] = (0, react.useState)("info");
|
|
3551
|
+
const notify = (text, tone = "info") => {
|
|
3552
|
+
setToastState(text);
|
|
3553
|
+
setToastTone(tone);
|
|
3554
|
+
};
|
|
3555
|
+
const [showHtml, setShowHtml] = (0, react.useState)(false);
|
|
3219
3556
|
const [risFallback, setRisFallback] = (0, react.useState)(null);
|
|
3220
3557
|
const taskId = ref?.taskId ?? "";
|
|
3221
3558
|
ref?.paperId;
|
|
@@ -3245,27 +3582,20 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3245
3582
|
nonce,
|
|
3246
3583
|
t
|
|
3247
3584
|
]);
|
|
3248
|
-
const state = detail?.state;
|
|
3249
|
-
const noteKey = state?.note ?? "";
|
|
3250
|
-
const tagsKey = (state?.tags ?? []).join(",");
|
|
3251
|
-
const orderKey = state?.shortlistOrder ?? "";
|
|
3252
3585
|
(0, react.useEffect)(() => {
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
orderKey
|
|
3260
|
-
]);
|
|
3586
|
+
if (ref === null) return void 0;
|
|
3587
|
+
return subscribeTaskSummary((summary) => {
|
|
3588
|
+
if (summary.id === ref.taskId) setNonce((value) => value + 1);
|
|
3589
|
+
});
|
|
3590
|
+
}, [ref?.taskId]);
|
|
3591
|
+
const state = detail?.state;
|
|
3261
3592
|
(0, react.useEffect)(() => {
|
|
3262
|
-
|
|
3263
|
-
setPreview(null);
|
|
3593
|
+
setShowHtml(false);
|
|
3264
3594
|
setRisFallback(null);
|
|
3265
3595
|
}, [ref?.taskId, ref?.paperId]);
|
|
3266
3596
|
(0, react.useEffect)(() => {
|
|
3267
3597
|
if (toast === "") return void 0;
|
|
3268
|
-
const timer = setTimeout(() =>
|
|
3598
|
+
const timer = setTimeout(() => setToastState(""), 4e3);
|
|
3269
3599
|
return () => clearTimeout(timer);
|
|
3270
3600
|
}, [toast]);
|
|
3271
3601
|
const paper = detail?.paper;
|
|
@@ -3293,33 +3623,23 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3293
3623
|
const annotate = (input) => {
|
|
3294
3624
|
if (busy) return;
|
|
3295
3625
|
setBusy(true);
|
|
3296
|
-
annotateTask(taskId, [input]).then(() => setNonce((value) => value + 1)).catch((err) =>
|
|
3297
|
-
};
|
|
3298
|
-
const saveDetail = () => {
|
|
3299
|
-
const trimmedOrder = order.trim();
|
|
3300
|
-
const parsedOrder = Number(trimmedOrder);
|
|
3301
|
-
const tagList = tags.split(/[,,;;]/u).map((tag) => tag.trim()).filter((tag) => tag !== "");
|
|
3302
|
-
annotate({
|
|
3303
|
-
paperId: paper.paperId,
|
|
3304
|
-
note,
|
|
3305
|
-
tags: tagList,
|
|
3306
|
-
shortlistOrder: trimmedOrder === "" || !Number.isFinite(parsedOrder) ? null : Math.floor(parsedOrder)
|
|
3307
|
-
});
|
|
3308
|
-
setEditing(false);
|
|
3626
|
+
annotateTask(taskId, [input]).then(() => setNonce((value) => value + 1)).catch((err) => notify(messageOf$3(err))).finally(() => setBusy(false));
|
|
3309
3627
|
};
|
|
3310
3628
|
const insert = () => {
|
|
3311
3629
|
const outcome = insertQuote(inputActions, paperText(t, paper), paperSummary(t, paper));
|
|
3312
|
-
if (outcome === "inserted")
|
|
3313
|
-
else
|
|
3630
|
+
if (outcome === "inserted") notify(t("result.inserted"));
|
|
3631
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
3314
3632
|
};
|
|
3315
3633
|
const download = () => {
|
|
3316
3634
|
setBusy(true);
|
|
3635
|
+
setDownloading(true);
|
|
3317
3636
|
downloadPaperPdf(taskId, paper.paperId).then((result) => {
|
|
3318
|
-
if (result.status === "downloaded")
|
|
3319
|
-
else if (result.status === "html-only")
|
|
3320
|
-
else
|
|
3321
|
-
}).catch((err) =>
|
|
3637
|
+
if (result.status === "downloaded") notify(tpl(t("pdf.downloaded"), { name: result.filename ?? "" }));
|
|
3638
|
+
else if (result.status === "html-only") notify(tpl(t("pdf.htmlFallback"), { message: result.error ?? "" }), "error");
|
|
3639
|
+
else notify(tpl(t("pdf.error"), { message: result.error ?? result.status }), "error");
|
|
3640
|
+
}).catch((err) => notify(tpl(t("pdf.error"), { message: messageOf$3(err) }), "error")).finally(() => {
|
|
3322
3641
|
setBusy(false);
|
|
3642
|
+
setDownloading(false);
|
|
3323
3643
|
setNonce((value) => value + 1);
|
|
3324
3644
|
});
|
|
3325
3645
|
};
|
|
@@ -3327,8 +3647,8 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3327
3647
|
setBusy(true);
|
|
3328
3648
|
syncZotero(taskId, [paper.paperId]).then((result) => {
|
|
3329
3649
|
const message = result.messages[0] ?? "";
|
|
3330
|
-
if (result.failed === 0 && message === "")
|
|
3331
|
-
else
|
|
3650
|
+
if (result.failed === 0 && message === "") notify(tpl(t("pdf.zoteroDone"), { n: result.saved }));
|
|
3651
|
+
else notify(tpl(t("pdf.zoteroPartial"), {
|
|
3332
3652
|
saved: result.saved,
|
|
3333
3653
|
failed: result.failed,
|
|
3334
3654
|
message
|
|
@@ -3339,7 +3659,7 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3339
3659
|
});
|
|
3340
3660
|
}).catch((err) => {
|
|
3341
3661
|
const message = messageOf$3(err);
|
|
3342
|
-
|
|
3662
|
+
notify(tpl(t("pdf.zoteroFailed"), { message }));
|
|
3343
3663
|
setRisFallback({
|
|
3344
3664
|
ids: [paper.paperId],
|
|
3345
3665
|
message
|
|
@@ -3349,6 +3669,12 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3349
3669
|
const pdfStatus = state?.pdfStatus ?? (paper.pdfUrl === "" ? "unknown" : "available");
|
|
3350
3670
|
const canPdf = paper.pdfUrl !== "" || paper.ids.pmcid !== "" || paper.ids.arxivId !== "" || paper.doi !== "";
|
|
3351
3671
|
const canHtml = paper.ids.pmcid !== "";
|
|
3672
|
+
const downloaded = !downloading && state?.pdfStatus === "downloaded";
|
|
3673
|
+
const downloadedPath = downloaded ? state?.pdfFile : void 0;
|
|
3674
|
+
const pdfMissing = !downloading && state?.pdfStatus === "missing";
|
|
3675
|
+
const failedMessage = !downloading && state?.pdfStatus === "failed" ? state.pdfError : void 0;
|
|
3676
|
+
const downloadLabel = downloading ? t("pdf.downloading") : downloaded ? t("pdf.downloadedAction") : pdfMissing ? t("pdf.missingAction") : state?.pdfStatus === "failed" ? t("pdf.retry") : t("pdf.download");
|
|
3677
|
+
const downloadHint = downloading ? t("pdf.downloadingHint") : downloaded ? tpl(t("pdf.downloadedHint"), { path: downloadedPath ?? detail.downloadDir ?? "" }) : pdfMissing ? t("pdf.missingHint") : failedMessage !== void 0 ? tpl(t("pdf.retryHint"), { message: failedMessage }) : tpl(t("pdf.downloadHint"), { path: detail.downloadDir ?? t("pdf.downloadDirUnknown") });
|
|
3352
3678
|
const meta = [
|
|
3353
3679
|
paper.authors.length === 0 ? "" : `${paper.authors.slice(0, 6).join(", ")}${paper.authors.length > 6 ? " et al." : ""}`,
|
|
3354
3680
|
paper.year === null ? "" : String(paper.year),
|
|
@@ -3356,7 +3682,10 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3356
3682
|
].filter((part) => part !== "").join(" · ");
|
|
3357
3683
|
const groupName = detail.groups.find((group) => group.id === state?.groupId)?.name;
|
|
3358
3684
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3359
|
-
style:
|
|
3685
|
+
style: {
|
|
3686
|
+
...panelStyle,
|
|
3687
|
+
position: "relative"
|
|
3688
|
+
},
|
|
3360
3689
|
children: [
|
|
3361
3690
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3362
3691
|
style: {
|
|
@@ -3392,10 +3721,6 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3392
3721
|
style: badgeStyle,
|
|
3393
3722
|
children: groupName
|
|
3394
3723
|
}),
|
|
3395
|
-
state?.shortlistOrder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3396
|
-
style: badgeStyle,
|
|
3397
|
-
children: tpl(t("annotate.shortlistBadge"), { n: state.shortlistOrder })
|
|
3398
|
-
}),
|
|
3399
3724
|
sources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3400
3725
|
style: badgeStyle,
|
|
3401
3726
|
children: t(`source.${source}`)
|
|
@@ -3417,106 +3742,39 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3417
3742
|
})
|
|
3418
3743
|
]
|
|
3419
3744
|
}),
|
|
3420
|
-
state?.pdfError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3421
|
-
style: {
|
|
3422
|
-
...statusStyle,
|
|
3423
|
-
color: "#b26a00"
|
|
3424
|
-
},
|
|
3425
|
-
children: tpl(t("pdf.error"), { message: state.pdfError })
|
|
3426
|
-
}),
|
|
3427
3745
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3428
3746
|
style: STATE_ROW_STYLE$1,
|
|
3429
|
-
children: [
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
screening: event.target.value
|
|
3438
|
-
}),
|
|
3439
|
-
children: SCREENINGS$3.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3440
|
-
value,
|
|
3441
|
-
children: t(`screening.${value}`)
|
|
3442
|
-
}, value))
|
|
3443
|
-
}),
|
|
3444
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3445
|
-
style: SELECT_STYLE$3,
|
|
3446
|
-
value: state?.groupId ?? "",
|
|
3447
|
-
disabled: busy,
|
|
3448
|
-
"aria-label": t("annotate.group"),
|
|
3449
|
-
title: detail.groups.length === 0 ? t("annotate.groupHint") : t("annotate.group"),
|
|
3450
|
-
onChange: (event) => annotate({
|
|
3451
|
-
paperId: paper.paperId,
|
|
3452
|
-
groupId: event.target.value === "" ? null : event.target.value
|
|
3453
|
-
}),
|
|
3454
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3455
|
-
value: "",
|
|
3456
|
-
children: t("annotate.noGroup")
|
|
3457
|
-
}), detail.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3458
|
-
value: group.id,
|
|
3459
|
-
children: group.name
|
|
3460
|
-
}, group.id))]
|
|
3461
|
-
}),
|
|
3462
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3463
|
-
type: "button",
|
|
3464
|
-
style: linkButtonStyle,
|
|
3465
|
-
onClick: () => setEditing((value) => !value),
|
|
3466
|
-
children: editing ? t("annotate.collapse") : t("annotate.expand")
|
|
3467
|
-
})
|
|
3468
|
-
]
|
|
3469
|
-
}),
|
|
3470
|
-
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3471
|
-
style: EDIT_STYLE$1,
|
|
3472
|
-
children: [
|
|
3473
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3474
|
-
style: FIELD_STYLE$4,
|
|
3475
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.note") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
3476
|
-
style: NOTE_STYLE$1,
|
|
3477
|
-
value: note,
|
|
3478
|
-
rows: 3,
|
|
3479
|
-
placeholder: t("annotate.notePlaceholder"),
|
|
3480
|
-
onChange: (event) => setNote(event.target.value)
|
|
3481
|
-
})]
|
|
3482
|
-
}),
|
|
3483
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3484
|
-
style: FIELD_STYLE$4,
|
|
3485
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.tags") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3486
|
-
style: INPUT_STYLE$3,
|
|
3487
|
-
value: tags,
|
|
3488
|
-
placeholder: t("annotate.tagsPlaceholder"),
|
|
3489
|
-
onChange: (event) => setTags(event.target.value)
|
|
3490
|
-
})]
|
|
3747
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
3748
|
+
style: SELECT_STYLE$3,
|
|
3749
|
+
value: state?.screening ?? "unread",
|
|
3750
|
+
disabled: busy,
|
|
3751
|
+
"aria-label": t("filter.screening"),
|
|
3752
|
+
onChange: (event) => annotate({
|
|
3753
|
+
paperId: paper.paperId,
|
|
3754
|
+
screening: event.target.value
|
|
3491
3755
|
}),
|
|
3492
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3756
|
+
children: SCREENINGS$3.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3757
|
+
value,
|
|
3758
|
+
children: t(`screening.${value}`)
|
|
3759
|
+
}, value))
|
|
3760
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3761
|
+
style: SELECT_STYLE$3,
|
|
3762
|
+
value: state?.groupId ?? "",
|
|
3763
|
+
disabled: busy,
|
|
3764
|
+
"aria-label": t("annotate.group"),
|
|
3765
|
+
title: detail.groups.length === 0 ? t("annotate.groupHint") : t("annotate.group"),
|
|
3766
|
+
onChange: (event) => annotate({
|
|
3767
|
+
paperId: paper.paperId,
|
|
3768
|
+
groupId: event.target.value === "" ? null : event.target.value
|
|
3504
3769
|
}),
|
|
3505
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
children:
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
type: "button",
|
|
3514
|
-
disabled: busy,
|
|
3515
|
-
onClick: saveDetail,
|
|
3516
|
-
children: t("annotate.save")
|
|
3517
|
-
})
|
|
3518
|
-
})
|
|
3519
|
-
]
|
|
3770
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3771
|
+
value: "",
|
|
3772
|
+
children: t("annotate.noGroup")
|
|
3773
|
+
}), detail.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3774
|
+
value: group.id,
|
|
3775
|
+
children: group.name
|
|
3776
|
+
}, group.id))]
|
|
3777
|
+
})]
|
|
3520
3778
|
}),
|
|
3521
3779
|
paper.abstract !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
3522
3780
|
style: HEAD_STYLE$2,
|
|
@@ -3577,49 +3835,45 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3577
3835
|
paperId: paper.paperId,
|
|
3578
3836
|
pmid: paper.ids.pmid
|
|
3579
3837
|
})] }),
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
type: "button",
|
|
3586
|
-
style: linkButtonStyle,
|
|
3587
|
-
onClick: () => setPreview(null),
|
|
3588
|
-
children: t("pdf.closePreview")
|
|
3589
|
-
})]
|
|
3590
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
3591
|
-
title: preview === "pdf" ? t("pdf.preview") : t("pdf.fulltext"),
|
|
3592
|
-
src: preview === "pdf" ? paperPdfUrl(taskId, paper.paperId) : paperFullTextUrl(taskId, paper.paperId),
|
|
3593
|
-
style: IFRAME_STYLE$1
|
|
3594
|
-
})]
|
|
3838
|
+
showHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreviewFrame, {
|
|
3839
|
+
t,
|
|
3840
|
+
src: paperFullTextUrl(taskId, paper.paperId),
|
|
3841
|
+
height: 420,
|
|
3842
|
+
onClose: () => setShowHtml(false)
|
|
3595
3843
|
}),
|
|
3596
|
-
toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3597
|
-
style:
|
|
3598
|
-
children: toast
|
|
3844
|
+
(toast !== "" || risFallback !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3845
|
+
style: toastStackStyle,
|
|
3846
|
+
children: [toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3847
|
+
role: "status",
|
|
3848
|
+
style: {
|
|
3849
|
+
...toastStyle,
|
|
3850
|
+
...toastTone === "error" ? toastErrorStyle : null
|
|
3851
|
+
},
|
|
3852
|
+
children: toast
|
|
3853
|
+
}), risFallback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3854
|
+
style: FALLBACK_STYLE,
|
|
3855
|
+
children: [
|
|
3856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3857
|
+
style: { minWidth: 0 },
|
|
3858
|
+
children: tpl(t("pdf.zoteroFallback"), { message: risFallback.message })
|
|
3859
|
+
}),
|
|
3860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3861
|
+
href: exportUrl(taskId, "ris", {}, risFallback.ids),
|
|
3862
|
+
download: true,
|
|
3863
|
+
style: FALLBACK_LINK_STYLE,
|
|
3864
|
+
children: t("pdf.zoteroFallbackLink")
|
|
3865
|
+
}),
|
|
3866
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3867
|
+
type: "button",
|
|
3868
|
+
style: FALLBACK_BUTTON_STYLE,
|
|
3869
|
+
onClick: () => setRisFallback(null),
|
|
3870
|
+
children: t("pdf.zoteroFallbackDismiss")
|
|
3871
|
+
})
|
|
3872
|
+
]
|
|
3873
|
+
})]
|
|
3599
3874
|
}),
|
|
3600
|
-
|
|
3601
|
-
style:
|
|
3602
|
-
children: [
|
|
3603
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3604
|
-
style: { minWidth: 0 },
|
|
3605
|
-
children: tpl(t("pdf.zoteroFallback"), { message: risFallback.message })
|
|
3606
|
-
}),
|
|
3607
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3608
|
-
href: exportUrl(taskId, "ris", {}, risFallback.ids),
|
|
3609
|
-
download: true,
|
|
3610
|
-
style: linkStyle,
|
|
3611
|
-
children: t("pdf.zoteroFallbackLink")
|
|
3612
|
-
}),
|
|
3613
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3614
|
-
type: "button",
|
|
3615
|
-
style: linkButtonStyle,
|
|
3616
|
-
onClick: () => setRisFallback(null),
|
|
3617
|
-
children: t("pdf.zoteroFallbackDismiss")
|
|
3618
|
-
})
|
|
3619
|
-
]
|
|
3620
|
-
}),
|
|
3621
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3622
|
-
style: ACTION_STYLE,
|
|
3875
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3876
|
+
style: ACTION_STYLE,
|
|
3623
3877
|
children: [
|
|
3624
3878
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3625
3879
|
variant: "ghost",
|
|
@@ -3628,23 +3882,23 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3628
3882
|
onClick: insert,
|
|
3629
3883
|
children: t("result.insert")
|
|
3630
3884
|
}),
|
|
3631
|
-
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
type: "button",
|
|
3639
|
-
style: linkButtonStyle,
|
|
3885
|
+
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
|
|
3886
|
+
t,
|
|
3887
|
+
label: downloadLabel,
|
|
3888
|
+
hint: downloadHint,
|
|
3889
|
+
...downloadedPath === void 0 ? {} : { downloadedPath },
|
|
3890
|
+
missing: pdfMissing,
|
|
3891
|
+
downloading,
|
|
3640
3892
|
disabled: busy,
|
|
3641
|
-
|
|
3642
|
-
|
|
3893
|
+
style: linkButtonStyle,
|
|
3894
|
+
onDownload: download,
|
|
3895
|
+
...onOpenLocal === void 0 ? {} : { onOpen: (path) => onOpenLocal(taskId, paper.paperId, path) },
|
|
3896
|
+
...onRevealLocal === void 0 ? {} : { onReveal: onRevealLocal }
|
|
3643
3897
|
}),
|
|
3644
3898
|
canHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3645
3899
|
type: "button",
|
|
3646
3900
|
style: linkButtonStyle,
|
|
3647
|
-
onClick: () =>
|
|
3901
|
+
onClick: () => setShowHtml((value) => !value),
|
|
3648
3902
|
children: t("pdf.fulltext")
|
|
3649
3903
|
}),
|
|
3650
3904
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -3701,73 +3955,30 @@ const SELECT_STYLE$3 = {
|
|
|
3701
3955
|
color: "inherit",
|
|
3702
3956
|
maxWidth: 150
|
|
3703
3957
|
};
|
|
3704
|
-
const EDIT_STYLE$1 = {
|
|
3705
|
-
display: "flex",
|
|
3706
|
-
flexDirection: "column",
|
|
3707
|
-
gap: 6,
|
|
3708
|
-
padding: "6px 8px",
|
|
3709
|
-
borderRadius: 6,
|
|
3710
|
-
background: "rgba(127,127,127,0.07)"
|
|
3711
|
-
};
|
|
3712
|
-
const FIELD_STYLE$4 = {
|
|
3713
|
-
display: "flex",
|
|
3714
|
-
flexDirection: "column",
|
|
3715
|
-
gap: 2,
|
|
3716
|
-
fontSize: 11,
|
|
3717
|
-
opacity: .9
|
|
3718
|
-
};
|
|
3719
|
-
const INPUT_STYLE$3 = {
|
|
3720
|
-
fontSize: 12,
|
|
3721
|
-
padding: "3px 5px",
|
|
3722
|
-
borderRadius: 4,
|
|
3723
|
-
border: "1px solid rgba(127,127,127,0.28)",
|
|
3724
|
-
background: "transparent",
|
|
3725
|
-
color: "inherit",
|
|
3726
|
-
minWidth: 0
|
|
3727
|
-
};
|
|
3728
|
-
const NOTE_STYLE$1 = {
|
|
3729
|
-
...INPUT_STYLE$3,
|
|
3730
|
-
resize: "vertical",
|
|
3731
|
-
fontFamily: "inherit",
|
|
3732
|
-
lineHeight: 1.5
|
|
3733
|
-
};
|
|
3734
3958
|
const IDS_STYLE = {
|
|
3735
3959
|
display: "flex",
|
|
3736
3960
|
flexDirection: "column",
|
|
3737
3961
|
gap: 3,
|
|
3738
3962
|
fontSize: 12
|
|
3739
3963
|
};
|
|
3740
|
-
const PREVIEW_WRAP_STYLE$1 = {
|
|
3741
|
-
border: "1px solid rgba(127,127,127,0.22)",
|
|
3742
|
-
borderRadius: 6,
|
|
3743
|
-
overflow: "hidden"
|
|
3744
|
-
};
|
|
3745
|
-
const PREVIEW_HEAD_STYLE$1 = {
|
|
3746
|
-
display: "flex",
|
|
3747
|
-
alignItems: "center",
|
|
3748
|
-
justifyContent: "space-between",
|
|
3749
|
-
gap: 8,
|
|
3750
|
-
padding: "3px 8px",
|
|
3751
|
-
fontSize: 11,
|
|
3752
|
-
background: "rgba(127,127,127,0.08)"
|
|
3753
|
-
};
|
|
3754
|
-
const IFRAME_STYLE$1 = {
|
|
3755
|
-
width: "100%",
|
|
3756
|
-
height: 420,
|
|
3757
|
-
border: "none",
|
|
3758
|
-
background: "#fff"
|
|
3759
|
-
};
|
|
3760
3964
|
const FALLBACK_STYLE = {
|
|
3965
|
+
...toastStyle,
|
|
3966
|
+
pointerEvents: "auto",
|
|
3761
3967
|
display: "flex",
|
|
3762
3968
|
alignItems: "center",
|
|
3763
3969
|
flexWrap: "wrap",
|
|
3764
3970
|
gap: 8,
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
fontSize:
|
|
3770
|
-
|
|
3971
|
+
borderLeft: "3px solid #b26a00"
|
|
3972
|
+
};
|
|
3973
|
+
const FALLBACK_LINK_STYLE = {
|
|
3974
|
+
color: "#fff",
|
|
3975
|
+
fontSize: 12,
|
|
3976
|
+
textDecoration: "underline"
|
|
3977
|
+
};
|
|
3978
|
+
const FALLBACK_BUTTON_STYLE = {
|
|
3979
|
+
...linkButtonStyle,
|
|
3980
|
+
color: "#fff",
|
|
3981
|
+
textDecoration: "underline"
|
|
3771
3982
|
};
|
|
3772
3983
|
const ACTION_STYLE = {
|
|
3773
3984
|
display: "flex",
|
|
@@ -4971,7 +5182,7 @@ function PaperRow({ paper }) {
|
|
|
4971
5182
|
paper.venue
|
|
4972
5183
|
].filter((part) => part !== "").join(" · ");
|
|
4973
5184
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4974
|
-
style: ROW_STYLE$
|
|
5185
|
+
style: ROW_STYLE$4,
|
|
4975
5186
|
children: [
|
|
4976
5187
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4977
5188
|
style: {
|
|
@@ -5038,7 +5249,7 @@ const LIST_STYLE$3 = {
|
|
|
5038
5249
|
display: "flex",
|
|
5039
5250
|
flexDirection: "column"
|
|
5040
5251
|
};
|
|
5041
|
-
const ROW_STYLE$
|
|
5252
|
+
const ROW_STYLE$4 = {
|
|
5042
5253
|
display: "flex",
|
|
5043
5254
|
flexDirection: "column",
|
|
5044
5255
|
gap: 1,
|
|
@@ -5149,7 +5360,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5149
5360
|
open: true,
|
|
5150
5361
|
onClose: onCancel,
|
|
5151
5362
|
title: t("condition.title"),
|
|
5152
|
-
className: WIDE_MODAL_CLASS,
|
|
5363
|
+
className: WIDE_MODAL_CLASS$1,
|
|
5153
5364
|
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5154
5365
|
style: {
|
|
5155
5366
|
display: "flex",
|
|
@@ -5171,7 +5382,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5171
5382
|
children: saving ? t("condition.saving") : t("condition.save")
|
|
5172
5383
|
})]
|
|
5173
5384
|
}),
|
|
5174
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5385
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS$1 }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5175
5386
|
style: BODY_STYLE,
|
|
5176
5387
|
children: [
|
|
5177
5388
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5213,7 +5424,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5213
5424
|
infos: health?.sources
|
|
5214
5425
|
}),
|
|
5215
5426
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5216
|
-
style: ROW_STYLE$
|
|
5427
|
+
style: ROW_STYLE$3,
|
|
5217
5428
|
children: [
|
|
5218
5429
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5219
5430
|
style: FIELD_STYLE$2,
|
|
@@ -5258,7 +5469,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5258
5469
|
]
|
|
5259
5470
|
}),
|
|
5260
5471
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5261
|
-
style: ROW_STYLE$
|
|
5472
|
+
style: ROW_STYLE$3,
|
|
5262
5473
|
children: [
|
|
5263
5474
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5264
5475
|
style: CHECK_STYLE$2,
|
|
@@ -5334,16 +5545,16 @@ const BODY_STYLE = {
|
|
|
5334
5545
|
overflowY: "auto"
|
|
5335
5546
|
};
|
|
5336
5547
|
/** 宿主 Modal 默认宽度 min(380px,100%),条件编辑器字段较多,单独放宽。 */
|
|
5337
|
-
const WIDE_MODAL_CLASS = "litsearch-condition-modal";
|
|
5338
|
-
const WIDE_MODAL_CSS = `
|
|
5339
|
-
.${WIDE_MODAL_CLASS}{width:min(680px,calc(100vw - 48px))}
|
|
5548
|
+
const WIDE_MODAL_CLASS$1 = "litsearch-condition-modal";
|
|
5549
|
+
const WIDE_MODAL_CSS$1 = `
|
|
5550
|
+
.${WIDE_MODAL_CLASS$1}{width:min(680px,calc(100vw - 48px))}
|
|
5340
5551
|
`;
|
|
5341
5552
|
const QUERY_LIST_STYLE = {
|
|
5342
5553
|
display: "flex",
|
|
5343
5554
|
flexDirection: "column",
|
|
5344
5555
|
gap: 6
|
|
5345
5556
|
};
|
|
5346
|
-
const ROW_STYLE$
|
|
5557
|
+
const ROW_STYLE$3 = {
|
|
5347
5558
|
display: "flex",
|
|
5348
5559
|
alignItems: "center",
|
|
5349
5560
|
flexWrap: "wrap",
|
|
@@ -5377,6 +5588,249 @@ const CHECK_STYLE$2 = {
|
|
|
5377
5588
|
gap: 4,
|
|
5378
5589
|
fontSize: 12
|
|
5379
5590
|
};
|
|
5591
|
+
// src/client/DownloadDialog.tsx
|
|
5592
|
+
const PHASE_COLOR = {
|
|
5593
|
+
pending: "rgba(127,127,127,0.9)",
|
|
5594
|
+
downloading: ACCENT,
|
|
5595
|
+
downloaded: "#2e9e5b",
|
|
5596
|
+
manual: "#b26a00",
|
|
5597
|
+
failed: ERROR
|
|
5598
|
+
};
|
|
5599
|
+
/** 单篇的展示状态:即时状态优先,其次回落到 Host 记录的文献状态。 */
|
|
5600
|
+
function phaseOf(paperId, states, downloads) {
|
|
5601
|
+
const runtime = downloads[paperId];
|
|
5602
|
+
if (runtime?.state === "failed" && isManualPdfFailure(runtime.reason)) return "manual";
|
|
5603
|
+
if (runtime?.state !== void 0) return runtime.state;
|
|
5604
|
+
const status = states[paperId]?.pdfStatus ?? "unknown";
|
|
5605
|
+
if (status === "downloaded") return "downloaded";
|
|
5606
|
+
if (status === "blocked" || status === "paywalled" || status === "html-only") return "manual";
|
|
5607
|
+
if (status === "failed" || status === "missing") return "failed";
|
|
5608
|
+
return "pending";
|
|
5609
|
+
}
|
|
5610
|
+
function errorOf(paperId, states, downloads) {
|
|
5611
|
+
return downloads[paperId]?.error ?? states[paperId]?.pdfError;
|
|
5612
|
+
}
|
|
5613
|
+
function DownloadDialog({ t, open, items, states, downloads, onClose }) {
|
|
5614
|
+
const phases = items.map((item) => phaseOf(item.paperId, states, downloads));
|
|
5615
|
+
const downloaded = phases.filter((phase) => phase === "downloaded").length;
|
|
5616
|
+
const downloading = phases.filter((phase) => phase === "downloading").length;
|
|
5617
|
+
const manual = phases.filter((phase) => phase === "manual").length;
|
|
5618
|
+
const failed = phases.filter((phase) => phase === "failed").length;
|
|
5619
|
+
const pending = phases.filter((phase) => phase === "pending").length;
|
|
5620
|
+
const total = items.length;
|
|
5621
|
+
const done = total - pending - downloading;
|
|
5622
|
+
const rate = total === 0 ? 0 : Math.round(done / total * 100);
|
|
5623
|
+
const complete = total > 0 && pending === 0 && downloading === 0;
|
|
5624
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
5625
|
+
open,
|
|
5626
|
+
onClose,
|
|
5627
|
+
title: t("batch.title"),
|
|
5628
|
+
description: tpl(t("batch.desc"), { n: total }),
|
|
5629
|
+
className: WIDE_MODAL_CLASS,
|
|
5630
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5631
|
+
style: FOOTER_STYLE,
|
|
5632
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5633
|
+
style: statusStyle,
|
|
5634
|
+
children: tpl(t("batch.progress"), {
|
|
5635
|
+
done,
|
|
5636
|
+
total
|
|
5637
|
+
})
|
|
5638
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
5639
|
+
variant: "outline",
|
|
5640
|
+
size: "sm",
|
|
5641
|
+
type: "button",
|
|
5642
|
+
onClick: onClose,
|
|
5643
|
+
children: t("batch.close")
|
|
5644
|
+
})]
|
|
5645
|
+
}),
|
|
5646
|
+
children: [
|
|
5647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS }),
|
|
5648
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5649
|
+
style: OVERVIEW_STYLE,
|
|
5650
|
+
role: "status",
|
|
5651
|
+
"aria-live": "polite",
|
|
5652
|
+
children: [
|
|
5653
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5654
|
+
style: COUNTS_STYLE,
|
|
5655
|
+
children: [
|
|
5656
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("batch.total"), { n: total }) }),
|
|
5657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5658
|
+
style: { color: PHASE_COLOR.downloaded },
|
|
5659
|
+
children: tpl(t("batch.downloaded"), { n: downloaded })
|
|
5660
|
+
}),
|
|
5661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5662
|
+
style: { color: PHASE_COLOR.downloading },
|
|
5663
|
+
children: tpl(t("batch.downloading"), { n: downloading })
|
|
5664
|
+
}),
|
|
5665
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("batch.pending"), { n: pending }) }),
|
|
5666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5667
|
+
style: { color: PHASE_COLOR.manual },
|
|
5668
|
+
children: tpl(t("batch.manual"), { n: manual })
|
|
5669
|
+
}),
|
|
5670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5671
|
+
style: { color: PHASE_COLOR.failed },
|
|
5672
|
+
children: tpl(t("batch.failed"), { n: failed })
|
|
5673
|
+
})
|
|
5674
|
+
]
|
|
5675
|
+
}),
|
|
5676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5677
|
+
style: BAR_TRACK_STYLE,
|
|
5678
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
5679
|
+
...BAR_FILL_STYLE$1,
|
|
5680
|
+
width: `${rate}%`
|
|
5681
|
+
} })
|
|
5682
|
+
}),
|
|
5683
|
+
complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5684
|
+
style: COMPLETE_STYLE,
|
|
5685
|
+
children: t("batch.complete")
|
|
5686
|
+
})
|
|
5687
|
+
]
|
|
5688
|
+
}),
|
|
5689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5690
|
+
style: LIST_STYLE$2,
|
|
5691
|
+
children: items.map((item, index) => {
|
|
5692
|
+
const phase = phases[index] ?? "pending";
|
|
5693
|
+
const error = phase === "failed" || phase === "manual" ? errorOf(item.paperId, states, downloads) : void 0;
|
|
5694
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
5695
|
+
style: ROW_STYLE$2,
|
|
5696
|
+
children: [
|
|
5697
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5698
|
+
style: INDEX_STYLE,
|
|
5699
|
+
children: index + 1
|
|
5700
|
+
}),
|
|
5701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5702
|
+
style: ROW_MAIN_STYLE$1,
|
|
5703
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5704
|
+
style: ROW_TITLE_STYLE$1,
|
|
5705
|
+
title: item.title,
|
|
5706
|
+
children: item.title === "" ? t("batch.untitled") : item.title
|
|
5707
|
+
}), error !== void 0 && error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5708
|
+
style: ROW_ERROR_STYLE$1,
|
|
5709
|
+
title: error,
|
|
5710
|
+
children: error
|
|
5711
|
+
})]
|
|
5712
|
+
}),
|
|
5713
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5714
|
+
style: {
|
|
5715
|
+
...STATUS_CHIP_STYLE,
|
|
5716
|
+
color: PHASE_COLOR[phase]
|
|
5717
|
+
},
|
|
5718
|
+
children: [phase === "downloading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpinnerIcon, {}), t(`batch.status.${phase}`)]
|
|
5719
|
+
})
|
|
5720
|
+
]
|
|
5721
|
+
}, item.paperId);
|
|
5722
|
+
})
|
|
5723
|
+
})
|
|
5724
|
+
]
|
|
5725
|
+
});
|
|
5726
|
+
}
|
|
5727
|
+
/** 批量下载弹窗的宽度约为默认弹窗的两倍(默认约 540px)。 */
|
|
5728
|
+
const WIDE_MODAL_CLASS = "litsearch-download-modal";
|
|
5729
|
+
const WIDE_MODAL_CSS = `
|
|
5730
|
+
.${WIDE_MODAL_CLASS}{width:min(1080px,calc(100vw - 48px))}
|
|
5731
|
+
`;
|
|
5732
|
+
const FOOTER_STYLE = {
|
|
5733
|
+
display: "flex",
|
|
5734
|
+
alignItems: "center",
|
|
5735
|
+
justifyContent: "space-between",
|
|
5736
|
+
gap: 8
|
|
5737
|
+
};
|
|
5738
|
+
const OVERVIEW_STYLE = {
|
|
5739
|
+
display: "flex",
|
|
5740
|
+
flexDirection: "column",
|
|
5741
|
+
gap: 6,
|
|
5742
|
+
marginBottom: 8,
|
|
5743
|
+
padding: "8px 10px",
|
|
5744
|
+
borderRadius: 8,
|
|
5745
|
+
border: "1px solid rgba(80,140,255,0.25)",
|
|
5746
|
+
background: "rgba(80,140,255,0.08)"
|
|
5747
|
+
};
|
|
5748
|
+
const COUNTS_STYLE = {
|
|
5749
|
+
display: "flex",
|
|
5750
|
+
flexWrap: "wrap",
|
|
5751
|
+
gap: 10,
|
|
5752
|
+
fontSize: 12,
|
|
5753
|
+
lineHeight: 1.5
|
|
5754
|
+
};
|
|
5755
|
+
const BAR_TRACK_STYLE = {
|
|
5756
|
+
height: 4,
|
|
5757
|
+
borderRadius: 2,
|
|
5758
|
+
background: "rgba(127,127,127,0.22)",
|
|
5759
|
+
overflow: "hidden"
|
|
5760
|
+
};
|
|
5761
|
+
const BAR_FILL_STYLE$1 = {
|
|
5762
|
+
height: "100%",
|
|
5763
|
+
borderRadius: 2,
|
|
5764
|
+
background: ACCENT,
|
|
5765
|
+
transition: "width 0.25s ease"
|
|
5766
|
+
};
|
|
5767
|
+
const COMPLETE_STYLE = {
|
|
5768
|
+
fontSize: 12,
|
|
5769
|
+
fontWeight: 650,
|
|
5770
|
+
color: "#2e9e5b"
|
|
5771
|
+
};
|
|
5772
|
+
const LIST_STYLE$2 = {
|
|
5773
|
+
listStyle: "none",
|
|
5774
|
+
margin: 0,
|
|
5775
|
+
padding: 0,
|
|
5776
|
+
maxHeight: "50vh",
|
|
5777
|
+
overflowY: "auto"
|
|
5778
|
+
};
|
|
5779
|
+
const ROW_STYLE$2 = {
|
|
5780
|
+
display: "flex",
|
|
5781
|
+
alignItems: "flex-start",
|
|
5782
|
+
gap: 8,
|
|
5783
|
+
padding: "7px 0",
|
|
5784
|
+
borderTop: "1px solid rgba(127,127,127,0.18)"
|
|
5785
|
+
};
|
|
5786
|
+
const INDEX_STYLE = {
|
|
5787
|
+
flexShrink: 0,
|
|
5788
|
+
minWidth: 18,
|
|
5789
|
+
fontSize: 11,
|
|
5790
|
+
lineHeight: "18px",
|
|
5791
|
+
opacity: .5,
|
|
5792
|
+
textAlign: "right"
|
|
5793
|
+
};
|
|
5794
|
+
const ROW_MAIN_STYLE$1 = {
|
|
5795
|
+
display: "flex",
|
|
5796
|
+
flexDirection: "column",
|
|
5797
|
+
gap: 2,
|
|
5798
|
+
flex: 1,
|
|
5799
|
+
minWidth: 0
|
|
5800
|
+
};
|
|
5801
|
+
const ROW_TITLE_STYLE$1 = {
|
|
5802
|
+
fontSize: 12.5,
|
|
5803
|
+
lineHeight: 1.45,
|
|
5804
|
+
fontWeight: 600,
|
|
5805
|
+
overflow: "hidden",
|
|
5806
|
+
display: "-webkit-box",
|
|
5807
|
+
WebkitLineClamp: 2,
|
|
5808
|
+
WebkitBoxOrient: "vertical",
|
|
5809
|
+
wordBreak: "break-word"
|
|
5810
|
+
};
|
|
5811
|
+
const ROW_ERROR_STYLE$1 = {
|
|
5812
|
+
fontSize: 11,
|
|
5813
|
+
lineHeight: 1.4,
|
|
5814
|
+
color: ERROR,
|
|
5815
|
+
opacity: .85,
|
|
5816
|
+
overflow: "hidden",
|
|
5817
|
+
display: "-webkit-box",
|
|
5818
|
+
WebkitLineClamp: 2,
|
|
5819
|
+
WebkitBoxOrient: "vertical",
|
|
5820
|
+
wordBreak: "break-word"
|
|
5821
|
+
};
|
|
5822
|
+
const STATUS_CHIP_STYLE = {
|
|
5823
|
+
display: "inline-flex",
|
|
5824
|
+
alignItems: "center",
|
|
5825
|
+
gap: 3,
|
|
5826
|
+
flexShrink: 0,
|
|
5827
|
+
padding: "1px 6px",
|
|
5828
|
+
borderRadius: 4,
|
|
5829
|
+
border: "1px solid currentColor",
|
|
5830
|
+
fontSize: 11,
|
|
5831
|
+
lineHeight: 1.5,
|
|
5832
|
+
whiteSpace: "nowrap"
|
|
5833
|
+
};
|
|
5380
5834
|
// src/client/FullTextTab.tsx
|
|
5381
5835
|
/**
|
|
5382
5836
|
* 「全文」台账:全任务口径的覆盖率 + 可按状态切换的补抓队列。
|
|
@@ -5405,7 +5859,7 @@ const EMPTY_LIST = {
|
|
|
5405
5859
|
function viewQuery(view) {
|
|
5406
5860
|
return view === "missing" ? { missingFullText: true } : { pdfStatus: view };
|
|
5407
5861
|
}
|
|
5408
|
-
function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBatchDownload, onBatchZotero, onShowInResults }) {
|
|
5862
|
+
function FullTextTab({ t, taskId, rev, states, stats, onDownload, onOpenLocal, onRevealLocal, onZotero, onBatchDownload, onBatchZotero, onShowInResults }) {
|
|
5409
5863
|
const [view, setView] = (0, react.useState)("missing");
|
|
5410
5864
|
const [list, setList] = (0, react.useState)(EMPTY_LIST);
|
|
5411
5865
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
@@ -5484,7 +5938,7 @@ function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBa
|
|
|
5484
5938
|
});
|
|
5485
5939
|
const coverage = stats?.fullText;
|
|
5486
5940
|
const availableCount = coverage === void 0 ? 0 : coverage.available + coverage.downloaded + coverage.htmlOnly;
|
|
5487
|
-
const missingCount = coverage === void 0 ? 0 : coverage.unknown + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
5941
|
+
const missingCount = coverage === void 0 ? 0 : coverage.unknown + coverage.missing + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
5488
5942
|
const selectedPapers = list.papers.filter((paper) => selected.has(paper.paperId));
|
|
5489
5943
|
const viewLabel = view === "missing" ? t("fulltext.missing") : t(`pdf.status.${view}`);
|
|
5490
5944
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5555,15 +6009,18 @@ function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBa
|
|
|
5555
6009
|
style: TEXT_STYLE$1,
|
|
5556
6010
|
children: t("fulltext.none")
|
|
5557
6011
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5558
|
-
style: LIST_STYLE$
|
|
6012
|
+
style: LIST_STYLE$1,
|
|
5559
6013
|
children: list.papers.map((paper) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FullTextRow, {
|
|
5560
6014
|
t,
|
|
5561
6015
|
paper,
|
|
5562
6016
|
selected: selected.has(paper.paperId),
|
|
5563
6017
|
status: paperStatus(states, paper),
|
|
5564
6018
|
error: states[paper.paperId]?.pdfError,
|
|
6019
|
+
downloadedPath: states[paper.paperId]?.pdfFile,
|
|
5565
6020
|
onToggleSelect: toggleSelect,
|
|
5566
6021
|
onDownload,
|
|
6022
|
+
...onOpenLocal === void 0 ? {} : { onOpenLocal: (path) => onOpenLocal(taskId, paper.paperId, path) },
|
|
6023
|
+
...onRevealLocal === void 0 ? {} : { onRevealLocal },
|
|
5567
6024
|
onZotero
|
|
5568
6025
|
}, paper.paperId))
|
|
5569
6026
|
}),
|
|
@@ -5615,7 +6072,9 @@ function BulkBar$1({ t, count, onSelectPage, onClear, onDownload, onZotero }) {
|
|
|
5615
6072
|
]
|
|
5616
6073
|
});
|
|
5617
6074
|
}
|
|
5618
|
-
function FullTextRow({ t, paper, selected, status, error, onToggleSelect, onDownload, onZotero }) {
|
|
6075
|
+
function FullTextRow({ t, paper, selected, status, error, downloadedPath, onToggleSelect, onDownload, onOpenLocal, onRevealLocal, onZotero }) {
|
|
6076
|
+
const downloaded = status === "downloaded" && downloadedPath !== void 0;
|
|
6077
|
+
const pdfMissing = status === "missing";
|
|
5619
6078
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
5620
6079
|
style: ROW_STYLE$1,
|
|
5621
6080
|
children: [
|
|
@@ -5659,11 +6118,18 @@ function FullTextRow({ t, paper, selected, status, error, onToggleSelect, onDown
|
|
|
5659
6118
|
}),
|
|
5660
6119
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5661
6120
|
style: ROW_ACTIONS_STYLE,
|
|
5662
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5663
|
-
|
|
6121
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
|
|
6122
|
+
t,
|
|
6123
|
+
label: downloaded ? t("pdf.downloadedAction") : pdfMissing ? t("pdf.missingAction") : t("pdf.download"),
|
|
6124
|
+
...downloaded ? {
|
|
6125
|
+
downloadedPath,
|
|
6126
|
+
hint: tpl(t("pdf.downloadedHint"), { path: downloadedPath ?? "" })
|
|
6127
|
+
} : pdfMissing ? { hint: t("pdf.missingHint") } : {},
|
|
6128
|
+
missing: pdfMissing,
|
|
5664
6129
|
style: LINK_STYLE,
|
|
5665
|
-
|
|
5666
|
-
|
|
6130
|
+
onDownload: () => onDownload(paper),
|
|
6131
|
+
...onOpenLocal === void 0 ? {} : { onOpen: onOpenLocal },
|
|
6132
|
+
...onRevealLocal === void 0 ? {} : { onReveal: onRevealLocal }
|
|
5667
6133
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5668
6134
|
type: "button",
|
|
5669
6135
|
style: LINK_STYLE,
|
|
@@ -5754,7 +6220,7 @@ const MORE_STYLE = {
|
|
|
5754
6220
|
fontSize: 11,
|
|
5755
6221
|
cursor: "pointer"
|
|
5756
6222
|
};
|
|
5757
|
-
const LIST_STYLE$
|
|
6223
|
+
const LIST_STYLE$1 = {
|
|
5758
6224
|
margin: 0,
|
|
5759
6225
|
padding: 0,
|
|
5760
6226
|
listStyle: "none"
|
|
@@ -5802,19 +6268,18 @@ const ROW_ACTIONS_STYLE = {
|
|
|
5802
6268
|
/**
|
|
5803
6269
|
* 任务「分组」tab:维护「为什么读」的分类,并把文献归入其中。
|
|
5804
6270
|
*
|
|
5805
|
-
*
|
|
6271
|
+
* 分组是任务内概念;归组在结果卡片的分组下拉里完成,这里负责增删改名、排序。
|
|
5806
6272
|
* 增删与排序立即保存,名称和说明在输入框失焦时保存——切到结果页或别处不会丢改动。
|
|
5807
6273
|
*/
|
|
5808
6274
|
function draftOf(groups) {
|
|
5809
6275
|
return groups.map((group) => ({ ...group }));
|
|
5810
6276
|
}
|
|
5811
|
-
function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
6277
|
+
function GroupsTab({ t, taskId, detail, onSaved, onPickGroup, onNotify }) {
|
|
5812
6278
|
const [draft, setDraft] = (0, react.useState)(() => draftOf(detail.groups));
|
|
5813
6279
|
const [name, setName] = (0, react.useState)("");
|
|
5814
6280
|
const [description, setDescription] = (0, react.useState)("");
|
|
5815
6281
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
5816
6282
|
const [error, setError] = (0, react.useState)(null);
|
|
5817
|
-
const [toast, setToast] = (0, react.useState)("");
|
|
5818
6283
|
const latest = (0, react.useRef)(draft);
|
|
5819
6284
|
const queued = (0, react.useRef)(false);
|
|
5820
6285
|
const saving = (0, react.useRef)(false);
|
|
@@ -5836,14 +6301,13 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5836
6301
|
saving.current = true;
|
|
5837
6302
|
setBusy(true);
|
|
5838
6303
|
setError(null);
|
|
5839
|
-
setToast("");
|
|
5840
6304
|
(async () => {
|
|
5841
6305
|
try {
|
|
5842
6306
|
while (queued.current) {
|
|
5843
6307
|
queued.current = false;
|
|
5844
6308
|
await updateTask(taskId, { groups: latest.current.map((group) => ({ ...group })) });
|
|
5845
6309
|
}
|
|
5846
|
-
|
|
6310
|
+
onNotify(t("groups.saved"));
|
|
5847
6311
|
onSaved();
|
|
5848
6312
|
} catch (reason) {
|
|
5849
6313
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
@@ -5855,7 +6319,8 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5855
6319
|
}, [
|
|
5856
6320
|
taskId,
|
|
5857
6321
|
t,
|
|
5858
|
-
onSaved
|
|
6322
|
+
onSaved,
|
|
6323
|
+
onNotify
|
|
5859
6324
|
]);
|
|
5860
6325
|
const add = () => {
|
|
5861
6326
|
const trimmed = name.trim();
|
|
@@ -5908,7 +6373,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5908
6373
|
style: LABEL_STYLE,
|
|
5909
6374
|
children: t("groups.name")
|
|
5910
6375
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5911
|
-
style: INPUT_STYLE$
|
|
6376
|
+
style: INPUT_STYLE$1,
|
|
5912
6377
|
value: name,
|
|
5913
6378
|
placeholder: t("groups.namePlaceholder"),
|
|
5914
6379
|
"aria-label": t("groups.name"),
|
|
@@ -5927,7 +6392,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5927
6392
|
style: LABEL_STYLE,
|
|
5928
6393
|
children: t("groups.desc")
|
|
5929
6394
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5930
|
-
style: INPUT_STYLE$
|
|
6395
|
+
style: INPUT_STYLE$1,
|
|
5931
6396
|
value: description,
|
|
5932
6397
|
placeholder: t("groups.descPlaceholder"),
|
|
5933
6398
|
"aria-label": t("groups.desc"),
|
|
@@ -5953,7 +6418,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5953
6418
|
style: statusStyle,
|
|
5954
6419
|
children: t("groups.empty")
|
|
5955
6420
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5956
|
-
style: LIST_STYLE
|
|
6421
|
+
style: LIST_STYLE,
|
|
5957
6422
|
children: draft.map((group, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5958
6423
|
style: GROUP_ROW_STYLE,
|
|
5959
6424
|
children: [
|
|
@@ -5961,7 +6426,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5961
6426
|
style: GROUP_HEAD_STYLE,
|
|
5962
6427
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5963
6428
|
style: {
|
|
5964
|
-
...INPUT_STYLE$
|
|
6429
|
+
...INPUT_STYLE$1,
|
|
5965
6430
|
flex: 1,
|
|
5966
6431
|
fontWeight: 600
|
|
5967
6432
|
},
|
|
@@ -6024,15 +6489,12 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
6024
6489
|
]
|
|
6025
6490
|
}, group.id))
|
|
6026
6491
|
}),
|
|
6027
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
6492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6028
6493
|
style: ACTION_ROW_STYLE$1,
|
|
6029
|
-
children:
|
|
6030
|
-
style: TOAST_STYLE$1,
|
|
6031
|
-
children: toast
|
|
6032
|
-
}), busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6494
|
+
children: busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6033
6495
|
style: statusStyle,
|
|
6034
6496
|
children: t("groups.saving")
|
|
6035
|
-
})
|
|
6497
|
+
})
|
|
6036
6498
|
}),
|
|
6037
6499
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6038
6500
|
style: {
|
|
@@ -6071,7 +6533,7 @@ const LABEL_STYLE = {
|
|
|
6071
6533
|
opacity: .7,
|
|
6072
6534
|
whiteSpace: "nowrap"
|
|
6073
6535
|
};
|
|
6074
|
-
const INPUT_STYLE$
|
|
6536
|
+
const INPUT_STYLE$1 = {
|
|
6075
6537
|
fontSize: 12,
|
|
6076
6538
|
padding: "3px 5px",
|
|
6077
6539
|
borderRadius: 4,
|
|
@@ -6081,13 +6543,13 @@ const INPUT_STYLE$2 = {
|
|
|
6081
6543
|
minWidth: 0,
|
|
6082
6544
|
flex: "1 1 120px"
|
|
6083
6545
|
};
|
|
6084
|
-
const LIST_STYLE
|
|
6546
|
+
const LIST_STYLE = {
|
|
6085
6547
|
display: "flex",
|
|
6086
6548
|
flexDirection: "column",
|
|
6087
6549
|
gap: 6
|
|
6088
6550
|
};
|
|
6089
6551
|
const ROW_INPUT_STYLE = {
|
|
6090
|
-
...INPUT_STYLE$
|
|
6552
|
+
...INPUT_STYLE$1,
|
|
6091
6553
|
flex: "0 0 auto"
|
|
6092
6554
|
};
|
|
6093
6555
|
const GROUP_ROW_STYLE = {
|
|
@@ -6113,55 +6575,25 @@ const ACTION_ROW_STYLE$1 = {
|
|
|
6113
6575
|
gap: 10,
|
|
6114
6576
|
flexWrap: "wrap"
|
|
6115
6577
|
};
|
|
6116
|
-
const TOAST_STYLE$1 = {
|
|
6117
|
-
fontSize: 12,
|
|
6118
|
-
color: "#2e9e5b"
|
|
6119
|
-
};
|
|
6120
6578
|
// src/client/NotesTab.tsx
|
|
6121
6579
|
/**
|
|
6122
|
-
* 任务「笔记」tab
|
|
6580
|
+
* 任务「笔记」tab:任务级研究笔记。
|
|
6123
6581
|
*
|
|
6124
|
-
*
|
|
6125
|
-
* `updateTask({notes})`,与模型工具看到的是同一份数据。
|
|
6582
|
+
* 任务笔记直接写回 `updateTask({notes})`,与模型工具看到的是同一份数据。
|
|
6126
6583
|
*/
|
|
6127
|
-
|
|
6128
|
-
function NotesTab({ t, taskId, detail, onSaved }) {
|
|
6584
|
+
function NotesTab({ t, taskId, detail, onSaved, onNotify }) {
|
|
6129
6585
|
const [notes, setNotes] = (0, react.useState)(detail.notes);
|
|
6130
6586
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
6131
|
-
const [toast, setToast] = (0, react.useState)("");
|
|
6132
6587
|
const [error, setError] = (0, react.useState)(null);
|
|
6133
|
-
const [titles, setTitles] = (0, react.useState)({});
|
|
6134
|
-
const [loadingTitles, setLoadingTitles] = (0, react.useState)(true);
|
|
6135
6588
|
(0, react.useEffect)(() => {
|
|
6136
6589
|
setNotes(detail.notes);
|
|
6137
6590
|
}, [detail.rev]);
|
|
6138
|
-
(0, react.useEffect)(() => {
|
|
6139
|
-
const controller = new AbortController();
|
|
6140
|
-
setLoadingTitles(true);
|
|
6141
|
-
fetchResults(taskId, { limit: NOTE_PAGE_LIMIT }, controller.signal).then((page) => {
|
|
6142
|
-
if (controller.signal.aborted) return;
|
|
6143
|
-
const map = {};
|
|
6144
|
-
for (const paper of page.papers) map[paper.paperId] = paper.title;
|
|
6145
|
-
setTitles(map);
|
|
6146
|
-
}).catch(() => void 0).finally(() => {
|
|
6147
|
-
if (!controller.signal.aborted) setLoadingTitles(false);
|
|
6148
|
-
});
|
|
6149
|
-
return () => {
|
|
6150
|
-
controller.abort();
|
|
6151
|
-
};
|
|
6152
|
-
}, [taskId, detail.rev]);
|
|
6153
|
-
const entries = (0, react.useMemo)(() => {
|
|
6154
|
-
return Object.entries(detail.states).filter(([, state]) => (state.note ?? "") !== "" || (state.tags ?? []).length > 0).map(([paperId, state]) => ({
|
|
6155
|
-
paperId,
|
|
6156
|
-
state
|
|
6157
|
-
}));
|
|
6158
|
-
}, [detail.states]);
|
|
6159
6591
|
const save = () => {
|
|
6160
6592
|
if (busy) return;
|
|
6161
6593
|
setBusy(true);
|
|
6162
6594
|
setError(null);
|
|
6163
6595
|
updateTask(taskId, { notes }).then(() => {
|
|
6164
|
-
|
|
6596
|
+
onNotify(t("notes.saved"));
|
|
6165
6597
|
onSaved();
|
|
6166
6598
|
}).catch((reason) => {
|
|
6167
6599
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
@@ -6169,9 +6601,9 @@ function NotesTab({ t, taskId, detail, onSaved }) {
|
|
|
6169
6601
|
setBusy(false);
|
|
6170
6602
|
});
|
|
6171
6603
|
};
|
|
6172
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
6604
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6173
6605
|
style: WRAP_STYLE$2,
|
|
6174
|
-
children:
|
|
6606
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
6175
6607
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
6176
6608
|
style: HEAD_STYLE,
|
|
6177
6609
|
children: t("notes.title")
|
|
@@ -6183,19 +6615,16 @@ function NotesTab({ t, taskId, detail, onSaved }) {
|
|
|
6183
6615
|
placeholder: t("notes.taskPlaceholder"),
|
|
6184
6616
|
onChange: (event) => setNotes(event.target.value)
|
|
6185
6617
|
}),
|
|
6186
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
6618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6187
6619
|
style: ACTION_ROW_STYLE,
|
|
6188
|
-
children:
|
|
6620
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
6189
6621
|
variant: "outline",
|
|
6190
6622
|
size: "sm",
|
|
6191
6623
|
type: "button",
|
|
6192
6624
|
disabled: busy || notes === detail.notes,
|
|
6193
6625
|
onClick: save,
|
|
6194
6626
|
children: t("notes.save")
|
|
6195
|
-
})
|
|
6196
|
-
style: TOAST_STYLE,
|
|
6197
|
-
children: toast
|
|
6198
|
-
})]
|
|
6627
|
+
})
|
|
6199
6628
|
}),
|
|
6200
6629
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6201
6630
|
style: {
|
|
@@ -6204,42 +6633,7 @@ function NotesTab({ t, taskId, detail, onSaved }) {
|
|
|
6204
6633
|
},
|
|
6205
6634
|
children: error
|
|
6206
6635
|
})
|
|
6207
|
-
] })
|
|
6208
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
6209
|
-
style: HEAD_STYLE,
|
|
6210
|
-
children: t("notes.paperSection")
|
|
6211
|
-
}),
|
|
6212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6213
|
-
style: statusStyle,
|
|
6214
|
-
children: t("notes.hint")
|
|
6215
|
-
}),
|
|
6216
|
-
loadingTitles && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6217
|
-
style: statusStyle,
|
|
6218
|
-
children: t("notes.loading")
|
|
6219
|
-
}) : entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6220
|
-
style: statusStyle,
|
|
6221
|
-
children: t("notes.empty")
|
|
6222
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6223
|
-
style: LIST_STYLE,
|
|
6224
|
-
children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6225
|
-
style: ENTRY_STYLE,
|
|
6226
|
-
children: [
|
|
6227
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6228
|
-
style: ENTRY_TITLE_STYLE,
|
|
6229
|
-
children: titles[entry.paperId] ?? entry.paperId
|
|
6230
|
-
}),
|
|
6231
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6232
|
-
style: ENTRY_META_STYLE,
|
|
6233
|
-
children: [t(`screening.${entry.state.screening}`), entry.state.tags === void 0 || entry.state.tags.length === 0 ? "" : ` · ${tpl(t("notes.tags"), { value: entry.state.tags.join(", ") })}`]
|
|
6234
|
-
}),
|
|
6235
|
-
entry.state.note !== void 0 && entry.state.note !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6236
|
-
style: ENTRY_NOTE_STYLE,
|
|
6237
|
-
children: entry.state.note
|
|
6238
|
-
})
|
|
6239
|
-
]
|
|
6240
|
-
}, entry.paperId))
|
|
6241
|
-
})
|
|
6242
|
-
] })]
|
|
6636
|
+
] })
|
|
6243
6637
|
});
|
|
6244
6638
|
}
|
|
6245
6639
|
const WRAP_STYLE$2 = {
|
|
@@ -6272,35 +6666,6 @@ const ACTION_ROW_STYLE = {
|
|
|
6272
6666
|
gap: 8,
|
|
6273
6667
|
marginTop: 6
|
|
6274
6668
|
};
|
|
6275
|
-
const LIST_STYLE = {
|
|
6276
|
-
display: "flex",
|
|
6277
|
-
flexDirection: "column"
|
|
6278
|
-
};
|
|
6279
|
-
const ENTRY_STYLE = {
|
|
6280
|
-
display: "flex",
|
|
6281
|
-
flexDirection: "column",
|
|
6282
|
-
gap: 2,
|
|
6283
|
-
padding: "6px 0",
|
|
6284
|
-
borderTop: "1px solid rgba(127,127,127,0.14)"
|
|
6285
|
-
};
|
|
6286
|
-
const ENTRY_TITLE_STYLE = {
|
|
6287
|
-
fontSize: 12.5,
|
|
6288
|
-
fontWeight: 650,
|
|
6289
|
-
lineHeight: 1.45
|
|
6290
|
-
};
|
|
6291
|
-
const ENTRY_META_STYLE = {
|
|
6292
|
-
fontSize: 11,
|
|
6293
|
-
opacity: .7
|
|
6294
|
-
};
|
|
6295
|
-
const ENTRY_NOTE_STYLE = {
|
|
6296
|
-
fontSize: 12,
|
|
6297
|
-
lineHeight: 1.55,
|
|
6298
|
-
opacity: .85
|
|
6299
|
-
};
|
|
6300
|
-
const TOAST_STYLE = {
|
|
6301
|
-
fontSize: 12,
|
|
6302
|
-
color: "#2e9e5b"
|
|
6303
|
-
};
|
|
6304
6669
|
// src/client/OverviewTab.tsx
|
|
6305
6670
|
/**
|
|
6306
6671
|
* 任务「概览」tab:一眼看清本轮检索发生了什么。
|
|
@@ -6349,6 +6714,11 @@ const FULLTEXT_STATUSES = [
|
|
|
6349
6714
|
key: "downloaded",
|
|
6350
6715
|
color: C.green
|
|
6351
6716
|
},
|
|
6717
|
+
{
|
|
6718
|
+
status: "missing",
|
|
6719
|
+
key: "missing",
|
|
6720
|
+
color: "#d97706"
|
|
6721
|
+
},
|
|
6352
6722
|
{
|
|
6353
6723
|
status: "html-only",
|
|
6354
6724
|
key: "htmlOnly",
|
|
@@ -7685,8 +8055,7 @@ const QUICK_FILTER_STYLE$1 = {
|
|
|
7685
8055
|
/**
|
|
7686
8056
|
* 任务结果里的单篇文献卡片。
|
|
7687
8057
|
*
|
|
7688
|
-
*
|
|
7689
|
-
* 备注 / 标签 / 必读顺序收在「标注」里,避免窄栏里堆满输入框。
|
|
8058
|
+
* 标题可点外链;徽章展示命中来源、被引、开放获取。筛读状态与分组就地修改。
|
|
7690
8059
|
*/
|
|
7691
8060
|
const ABSTRACT_PREVIEW = 260;
|
|
7692
8061
|
/** 分组下拉里「去新建分组」的哨兵值,不会与真实 group id 冲突。 */
|
|
@@ -7697,32 +8066,18 @@ const SCREENINGS$2 = [
|
|
|
7697
8066
|
"included",
|
|
7698
8067
|
"excluded"
|
|
7699
8068
|
];
|
|
8069
|
+
/** 出版商明确要求订阅 / 拒绝访问时,不再提供「下载 PDF」,避免必然失败的操作。 */
|
|
8070
|
+
const NO_PDF_STATUSES = /* @__PURE__ */ new Set(["paywalled", "blocked"]);
|
|
7700
8071
|
const SCREENING_COLOR = {
|
|
7701
8072
|
unread: "rgba(127,127,127,0.9)",
|
|
7702
8073
|
read: "#508cff",
|
|
7703
8074
|
included: "#2e9e5b",
|
|
7704
8075
|
excluded: "#d64545"
|
|
7705
8076
|
};
|
|
7706
|
-
function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, onToggleSelect, onInsert, onAnnotate, onDownload, onZotero, onOpenPaper, onManageGroups }) {
|
|
8077
|
+
function PaperCard({ paper, t, taskId, state, groups, busy, downloadDir, download, onOpenLocal, onRevealLocal, selected = false, onToggleSelect, onInsert, onAnnotate, onDownload, onZotero, onOpenPaper, onManageGroups }) {
|
|
7707
8078
|
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
7708
|
-
const [editing, setEditing] = (0, react.useState)(false);
|
|
7709
8079
|
const [entities, setEntities] = (0, react.useState)(false);
|
|
7710
|
-
const [
|
|
7711
|
-
const [note, setNote] = (0, react.useState)(state?.note ?? "");
|
|
7712
|
-
const [tags, setTags] = (0, react.useState)((state?.tags ?? []).join(", "));
|
|
7713
|
-
const [order, setOrder] = (0, react.useState)(state?.shortlistOrder === void 0 ? "" : String(state.shortlistOrder));
|
|
7714
|
-
const noteKey = state?.note ?? "";
|
|
7715
|
-
const tagsKey = (state?.tags ?? []).join(",");
|
|
7716
|
-
const orderKey = state?.shortlistOrder ?? "";
|
|
7717
|
-
(0, react.useEffect)(() => {
|
|
7718
|
-
setNote(noteKey);
|
|
7719
|
-
setTags(tagsKey);
|
|
7720
|
-
setOrder(orderKey === "" ? "" : String(orderKey));
|
|
7721
|
-
}, [
|
|
7722
|
-
noteKey,
|
|
7723
|
-
tagsKey,
|
|
7724
|
-
orderKey
|
|
7725
|
-
]);
|
|
8080
|
+
const [showHtml, setShowHtml] = (0, react.useState)(false);
|
|
7726
8081
|
const meta = [
|
|
7727
8082
|
paper.authors.length === 0 ? "" : `${paper.authors.slice(0, 3).join(", ")}${paper.authors.length > 3 ? " et al." : ""}`,
|
|
7728
8083
|
paper.year === null ? "" : String(paper.year),
|
|
@@ -7733,22 +8088,16 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7733
8088
|
const abstract = expanded || !canCollapse ? paper.abstract : `${paper.abstract.slice(0, ABSTRACT_PREVIEW)}…`;
|
|
7734
8089
|
const screening = state?.screening ?? "unread";
|
|
7735
8090
|
const groupName = groups.find((group) => group.id === state?.groupId)?.name;
|
|
7736
|
-
const hasNote = (state?.note ?? "") !== "" || (state?.tags ?? []).length > 0;
|
|
7737
8091
|
const pdfStatus = state?.pdfStatus ?? (paper.pdfUrl === "" ? "unknown" : "available");
|
|
7738
|
-
const canPdf = paper.pdfUrl !== "" || paper.ids.pmcid !== "" || paper.ids.arxivId !== "" || paper.doi !== "";
|
|
8092
|
+
const canPdf = !NO_PDF_STATUSES.has(pdfStatus) && (paper.pdfUrl !== "" || paper.ids.pmcid !== "" || paper.ids.arxivId !== "" || paper.doi !== "");
|
|
7739
8093
|
const canHtml = paper.ids.pmcid !== "";
|
|
7740
|
-
const
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
tags: tagList,
|
|
7748
|
-
shortlistOrder: trimmedOrder === "" || !Number.isFinite(parsedOrder) ? null : Math.floor(parsedOrder)
|
|
7749
|
-
});
|
|
7750
|
-
setEditing(false);
|
|
7751
|
-
};
|
|
8094
|
+
const downloading = download?.state === "downloading";
|
|
8095
|
+
const pdfMissing = !downloading && pdfStatus === "missing";
|
|
8096
|
+
const downloaded = !pdfMissing && (download?.state === "downloaded" || pdfStatus === "downloaded" && !downloading);
|
|
8097
|
+
const downloadedPath = downloaded ? download?.path ?? state?.pdfFile : void 0;
|
|
8098
|
+
const failedMessage = download?.state === "failed" ? download.error : pdfStatus === "failed" ? state?.pdfError : void 0;
|
|
8099
|
+
const pdfLabel = downloading ? t("pdf.downloading") : downloaded ? t("pdf.downloadedAction") : pdfMissing ? t("pdf.missingAction") : pdfStatus === "failed" ? t("pdf.retry") : t("pdf.download");
|
|
8100
|
+
const pdfHint = downloading ? t("pdf.downloadingHint") : downloaded ? tpl(t("pdf.downloadedHint"), { path: downloadedPath ?? downloadDir ?? "" }) : pdfMissing ? t("pdf.missingHint") : failedMessage !== void 0 ? tpl(t("pdf.retryHint"), { message: failedMessage }) : tpl(t("pdf.downloadHint"), { path: downloadDir ?? t("pdf.downloadDirUnknown") });
|
|
7752
8101
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
7753
8102
|
style: cardStyle,
|
|
7754
8103
|
children: [
|
|
@@ -7807,63 +8156,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7807
8156
|
children: t("annotate.createGroup")
|
|
7808
8157
|
})
|
|
7809
8158
|
]
|
|
7810
|
-
}),
|
|
7811
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7812
|
-
type: "button",
|
|
7813
|
-
style: linkButtonStyle,
|
|
7814
|
-
onClick: () => setEditing((value) => !value),
|
|
7815
|
-
children: [editing ? t("annotate.collapse") : t("annotate.expand"), hasNote ? " •" : ""]
|
|
7816
|
-
})
|
|
7817
|
-
]
|
|
7818
|
-
}),
|
|
7819
|
-
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7820
|
-
style: EDIT_STYLE,
|
|
7821
|
-
children: [
|
|
7822
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7823
|
-
style: EDIT_FIELD_STYLE,
|
|
7824
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.note") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
7825
|
-
style: NOTE_STYLE,
|
|
7826
|
-
value: note,
|
|
7827
|
-
rows: 2,
|
|
7828
|
-
placeholder: t("annotate.notePlaceholder"),
|
|
7829
|
-
onChange: (event) => setNote(event.target.value)
|
|
7830
|
-
})]
|
|
7831
|
-
}),
|
|
7832
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7833
|
-
style: EDIT_FIELD_STYLE,
|
|
7834
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.tags") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7835
|
-
style: INPUT_STYLE$1,
|
|
7836
|
-
value: tags,
|
|
7837
|
-
placeholder: t("annotate.tagsPlaceholder"),
|
|
7838
|
-
onChange: (event) => setTags(event.target.value)
|
|
7839
|
-
})]
|
|
7840
|
-
}),
|
|
7841
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7842
|
-
style: {
|
|
7843
|
-
...EDIT_FIELD_STYLE,
|
|
7844
|
-
maxWidth: 120
|
|
7845
|
-
},
|
|
7846
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.shortlistOrder") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7847
|
-
style: INPUT_STYLE$1,
|
|
7848
|
-
inputMode: "numeric",
|
|
7849
|
-
value: order,
|
|
7850
|
-
placeholder: "—",
|
|
7851
|
-
onChange: (event) => setOrder(event.target.value)
|
|
7852
|
-
})]
|
|
7853
|
-
}),
|
|
7854
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7855
|
-
style: {
|
|
7856
|
-
display: "flex",
|
|
7857
|
-
justifyContent: "flex-end"
|
|
7858
|
-
},
|
|
7859
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
7860
|
-
variant: "outline",
|
|
7861
|
-
size: "sm",
|
|
7862
|
-
type: "button",
|
|
7863
|
-
disabled: busy,
|
|
7864
|
-
onClick: saveDetail,
|
|
7865
|
-
children: t("annotate.save")
|
|
7866
|
-
})
|
|
7867
8159
|
})
|
|
7868
8160
|
]
|
|
7869
8161
|
}),
|
|
@@ -7931,10 +8223,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7931
8223
|
})] })]
|
|
7932
8224
|
})]
|
|
7933
8225
|
}),
|
|
7934
|
-
state?.pdfError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7935
|
-
style: PDF_ERROR_STYLE,
|
|
7936
|
-
children: tpl(t("pdf.error"), { message: state.pdfError })
|
|
7937
|
-
}),
|
|
7938
8226
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7939
8227
|
style: CARD_ROW_STYLE,
|
|
7940
8228
|
title: t("result.infoHint"),
|
|
@@ -7959,10 +8247,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7959
8247
|
style: INFO_BADGE_STYLE,
|
|
7960
8248
|
children: groupName
|
|
7961
8249
|
}),
|
|
7962
|
-
state?.shortlistOrder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7963
|
-
style: INFO_BADGE_STYLE,
|
|
7964
|
-
children: tpl(t("annotate.shortlistBadge"), { n: state.shortlistOrder })
|
|
7965
|
-
}),
|
|
7966
8250
|
sources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7967
8251
|
style: INFO_BADGE_STYLE,
|
|
7968
8252
|
children: t(`source.${source}`)
|
|
@@ -7980,6 +8264,7 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7980
8264
|
...INFO_BADGE_STYLE,
|
|
7981
8265
|
color: PDF_COLOR[pdfStatus]
|
|
7982
8266
|
},
|
|
8267
|
+
title: state?.pdfError,
|
|
7983
8268
|
children: t(`pdf.status.${pdfStatus}`)
|
|
7984
8269
|
})
|
|
7985
8270
|
]
|
|
@@ -8020,23 +8305,23 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
8020
8305
|
onClick: () => setEntities((value) => !value),
|
|
8021
8306
|
children: t("entity.title")
|
|
8022
8307
|
}),
|
|
8023
|
-
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
type: "button",
|
|
8031
|
-
style: linkButtonStyle,
|
|
8308
|
+
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
|
|
8309
|
+
t,
|
|
8310
|
+
label: pdfLabel,
|
|
8311
|
+
hint: pdfHint,
|
|
8312
|
+
...downloadedPath === void 0 ? {} : { downloadedPath },
|
|
8313
|
+
missing: pdfMissing,
|
|
8314
|
+
downloading,
|
|
8032
8315
|
disabled: busy,
|
|
8033
|
-
|
|
8034
|
-
|
|
8316
|
+
style: linkButtonStyle,
|
|
8317
|
+
onDownload: () => onDownload(paper),
|
|
8318
|
+
...onOpenLocal === void 0 ? {} : { onOpen: (path) => onOpenLocal(taskId, paper.paperId, path) },
|
|
8319
|
+
...onRevealLocal === void 0 ? {} : { onReveal: onRevealLocal }
|
|
8035
8320
|
}),
|
|
8036
8321
|
canHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8037
8322
|
type: "button",
|
|
8038
8323
|
style: linkButtonStyle,
|
|
8039
|
-
onClick: () =>
|
|
8324
|
+
onClick: () => setShowHtml((value) => !value),
|
|
8040
8325
|
children: t("pdf.fulltext")
|
|
8041
8326
|
}),
|
|
8042
8327
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -8069,21 +8354,11 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
8069
8354
|
paperId: paper.paperId,
|
|
8070
8355
|
pmid: paper.ids.pmid
|
|
8071
8356
|
}),
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
type: "button",
|
|
8078
|
-
style: linkButtonStyle,
|
|
8079
|
-
onClick: () => setPreview(null),
|
|
8080
|
-
children: t("pdf.closePreview")
|
|
8081
|
-
})]
|
|
8082
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
8083
|
-
title: preview === "pdf" ? t("pdf.preview") : t("pdf.fulltext"),
|
|
8084
|
-
src: preview === "pdf" ? paperPdfUrl(taskId, paper.paperId) : paperFullTextUrl(taskId, paper.paperId),
|
|
8085
|
-
style: IFRAME_STYLE
|
|
8086
|
-
})]
|
|
8357
|
+
showHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreviewFrame, {
|
|
8358
|
+
t,
|
|
8359
|
+
src: paperFullTextUrl(taskId, paper.paperId),
|
|
8360
|
+
height: 360,
|
|
8361
|
+
onClose: () => setShowHtml(false)
|
|
8087
8362
|
})
|
|
8088
8363
|
]
|
|
8089
8364
|
});
|
|
@@ -8092,39 +8367,13 @@ const PDF_COLOR = {
|
|
|
8092
8367
|
unknown: "rgba(127,127,127,0.8)",
|
|
8093
8368
|
available: "#508cff",
|
|
8094
8369
|
downloaded: "#2e9e5b",
|
|
8370
|
+
missing: "#d64545",
|
|
8095
8371
|
"html-only": "#b26a00",
|
|
8096
8372
|
paywalled: "#b26a00",
|
|
8097
8373
|
blocked: "#d64545",
|
|
8098
8374
|
failed: "#d64545"
|
|
8099
8375
|
};
|
|
8100
|
-
|
|
8101
|
-
fontSize: 11,
|
|
8102
|
-
lineHeight: 1.5,
|
|
8103
|
-
color: "#b26a00",
|
|
8104
|
-
marginTop: 2
|
|
8105
|
-
};
|
|
8106
|
-
const PREVIEW_WRAP_STYLE = {
|
|
8107
|
-
marginTop: 6,
|
|
8108
|
-
border: "1px solid rgba(127,127,127,0.22)",
|
|
8109
|
-
borderRadius: 6,
|
|
8110
|
-
overflow: "hidden"
|
|
8111
|
-
};
|
|
8112
|
-
const PREVIEW_HEAD_STYLE = {
|
|
8113
|
-
display: "flex",
|
|
8114
|
-
alignItems: "center",
|
|
8115
|
-
justifyContent: "space-between",
|
|
8116
|
-
gap: 8,
|
|
8117
|
-
padding: "3px 8px",
|
|
8118
|
-
fontSize: 11,
|
|
8119
|
-
background: "rgba(127,127,127,0.08)"
|
|
8120
|
-
};
|
|
8121
|
-
const IFRAME_STYLE = {
|
|
8122
|
-
width: "100%",
|
|
8123
|
-
height: 360,
|
|
8124
|
-
border: "none",
|
|
8125
|
-
background: "#fff"
|
|
8126
|
-
};
|
|
8127
|
-
/** 卡片顶部工具行:勾选 / 筛读 / 分组 / 标注。 */
|
|
8376
|
+
/** 卡片顶部工具行:勾选 / 筛读 / 分组。 */
|
|
8128
8377
|
const STATE_ROW_STYLE = {
|
|
8129
8378
|
display: "flex",
|
|
8130
8379
|
alignItems: "center",
|
|
@@ -8195,37 +8444,6 @@ const SELECT_STYLE = {
|
|
|
8195
8444
|
color: "inherit",
|
|
8196
8445
|
maxWidth: 130
|
|
8197
8446
|
};
|
|
8198
|
-
const EDIT_STYLE = {
|
|
8199
|
-
display: "flex",
|
|
8200
|
-
flexDirection: "column",
|
|
8201
|
-
gap: 6,
|
|
8202
|
-
marginBottom: 8,
|
|
8203
|
-
padding: "6px 8px",
|
|
8204
|
-
borderRadius: 6,
|
|
8205
|
-
background: "rgba(127,127,127,0.07)"
|
|
8206
|
-
};
|
|
8207
|
-
const EDIT_FIELD_STYLE = {
|
|
8208
|
-
display: "flex",
|
|
8209
|
-
flexDirection: "column",
|
|
8210
|
-
gap: 2,
|
|
8211
|
-
fontSize: 11,
|
|
8212
|
-
opacity: .9
|
|
8213
|
-
};
|
|
8214
|
-
const INPUT_STYLE$1 = {
|
|
8215
|
-
fontSize: 12,
|
|
8216
|
-
padding: "3px 5px",
|
|
8217
|
-
borderRadius: 4,
|
|
8218
|
-
border: "1px solid rgba(127,127,127,0.28)",
|
|
8219
|
-
background: "transparent",
|
|
8220
|
-
color: "inherit",
|
|
8221
|
-
minWidth: 0
|
|
8222
|
-
};
|
|
8223
|
-
const NOTE_STYLE = {
|
|
8224
|
-
...INPUT_STYLE$1,
|
|
8225
|
-
resize: "vertical",
|
|
8226
|
-
fontFamily: "inherit",
|
|
8227
|
-
lineHeight: 1.5
|
|
8228
|
-
};
|
|
8229
8447
|
// src/client/RoundsDialog.tsx
|
|
8230
8448
|
/**
|
|
8231
8449
|
* 任务内「检索轮次」时间轴弹窗。
|
|
@@ -9250,15 +9468,33 @@ function formatDate(value) {
|
|
|
9250
9468
|
const date = new Date(value);
|
|
9251
9469
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
9252
9470
|
}
|
|
9253
|
-
function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onCloseTask }) {
|
|
9471
|
+
function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onCloseTask, onOpenLocal, onRevealLocal }) {
|
|
9254
9472
|
const info = useTabInfo();
|
|
9255
9473
|
const taskId = parseTaskId(info);
|
|
9256
9474
|
const [filters, setFilters] = (0, react.useState)({});
|
|
9257
9475
|
const [tab, setTab] = (0, react.useState)("results");
|
|
9258
9476
|
const view = useTask(taskId, info.tab.signal, 20, filters);
|
|
9259
9477
|
const bodyRef = (0, react.useRef)(null);
|
|
9260
|
-
const [toast,
|
|
9478
|
+
const [toast, setToastState] = (0, react.useState)("");
|
|
9479
|
+
const [toastTone, setToastTone] = (0, react.useState)("info");
|
|
9480
|
+
const [toastMs, setToastMs] = (0, react.useState)(4e3);
|
|
9481
|
+
/** 统一的底部浮层提示:tone 区分普通结果与下载失败。 */
|
|
9482
|
+
const notify = (text, tone = "info", ms = 4e3) => {
|
|
9483
|
+
setToastState(text);
|
|
9484
|
+
setToastTone(tone);
|
|
9485
|
+
setToastMs(ms);
|
|
9486
|
+
};
|
|
9487
|
+
/** paperId → 下载状态;刷新任务后仍保留「下载中 / 已下载」的即时反馈。 */
|
|
9488
|
+
const [downloads, setDownloads] = (0, react.useState)({});
|
|
9489
|
+
/** 批量下载弹窗:只集中展示,真实状态仍写在 downloads / 任务详情的 states 里。 */
|
|
9490
|
+
const [batchItems, setBatchItems] = (0, react.useState)([]);
|
|
9491
|
+
const [batchOpen, setBatchOpen] = (0, react.useState)(false);
|
|
9492
|
+
const [batchRunning, setBatchRunning] = (0, react.useState)(false);
|
|
9493
|
+
const batchRunningRef = (0, react.useRef)(false);
|
|
9261
9494
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
9495
|
+
/** 结果页里见过的文献:跨页选中的条目做「加入对话」时要能拿到论文详情。 */
|
|
9496
|
+
const papersByIdRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
9497
|
+
const ids = [...selected];
|
|
9262
9498
|
const [editingConditions, setEditingConditions] = (0, react.useState)(false);
|
|
9263
9499
|
const [risFallback, setRisFallback] = (0, react.useState)(null);
|
|
9264
9500
|
const [showRounds, setShowRounds] = (0, react.useState)(false);
|
|
@@ -9267,10 +9503,28 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9267
9503
|
const [overview, setOverview] = (0, react.useState)(null);
|
|
9268
9504
|
const detail = view.detail;
|
|
9269
9505
|
const filterActive = (0, react.useMemo)(() => activeFilterCount(filters) > 0, [filters]);
|
|
9270
|
-
const
|
|
9506
|
+
const selectionScope = JSON.stringify({
|
|
9507
|
+
...filters,
|
|
9508
|
+
sort: void 0
|
|
9509
|
+
});
|
|
9510
|
+
const downloadsRef = (0, react.useRef)(downloads);
|
|
9511
|
+
downloadsRef.current = downloads;
|
|
9512
|
+
const statesRef = (0, react.useRef)({});
|
|
9513
|
+
statesRef.current = detail?.states ?? {};
|
|
9271
9514
|
(0, react.useEffect)(() => {
|
|
9272
9515
|
setSelected(/* @__PURE__ */ new Set());
|
|
9273
|
-
}, [taskId,
|
|
9516
|
+
}, [taskId, selectionScope]);
|
|
9517
|
+
(0, react.useEffect)(() => {
|
|
9518
|
+
papersByIdRef.current.clear();
|
|
9519
|
+
}, [taskId]);
|
|
9520
|
+
(0, react.useEffect)(() => {
|
|
9521
|
+
for (const paper of view.page?.papers ?? []) papersByIdRef.current.set(paper.paperId, paper);
|
|
9522
|
+
}, [view.page]);
|
|
9523
|
+
(0, react.useEffect)(() => {
|
|
9524
|
+
setDownloads({});
|
|
9525
|
+
setBatchItems([]);
|
|
9526
|
+
setBatchOpen(false);
|
|
9527
|
+
}, [taskId]);
|
|
9274
9528
|
(0, react.useEffect)(() => {
|
|
9275
9529
|
setEditingConditions(false);
|
|
9276
9530
|
setRisFallback(null);
|
|
@@ -9293,43 +9547,83 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9293
9547
|
}, [taskId]);
|
|
9294
9548
|
(0, react.useEffect)(() => {
|
|
9295
9549
|
if (toast === "") return void 0;
|
|
9296
|
-
const timer = setTimeout(() =>
|
|
9550
|
+
const timer = setTimeout(() => notify(""), toastMs);
|
|
9297
9551
|
return () => clearTimeout(timer);
|
|
9298
|
-
}, [toast]);
|
|
9552
|
+
}, [toast, toastMs]);
|
|
9299
9553
|
(0, react.useEffect)(() => {
|
|
9300
9554
|
bodyRef.current?.scrollTo({ top: 0 });
|
|
9301
9555
|
}, [view.pageIndex, tab]);
|
|
9302
9556
|
const insert = (paper) => {
|
|
9303
9557
|
const outcome = insertQuote(inputActions, paperText(t, paper), paperSummary(t, paper));
|
|
9304
|
-
if (outcome === "inserted")
|
|
9305
|
-
else
|
|
9558
|
+
if (outcome === "inserted") notify(t("result.inserted"));
|
|
9559
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
9306
9560
|
};
|
|
9561
|
+
/** 跨页选中也要整批插入:优先用翻页时缓存的论文,缺的再按 id 补详情。 */
|
|
9307
9562
|
const insertMany = () => {
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
const
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9563
|
+
if (taskId === null || ids.length === 0) return;
|
|
9564
|
+
const known = papersByIdRef.current;
|
|
9565
|
+
const finish = () => {
|
|
9566
|
+
const papers = ids.map((paperId) => known.get(paperId)).filter((paper) => paper !== void 0);
|
|
9567
|
+
if (papers.length === 0) return;
|
|
9568
|
+
const outcome = insertQuote(inputActions, papersText(t, papers), papersSummary(t, papers));
|
|
9569
|
+
if (outcome === "inserted") notify(tpl(t("bulk.inserted"), { n: papers.length }));
|
|
9570
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
9571
|
+
};
|
|
9572
|
+
const missing = ids.filter((paperId) => !known.has(paperId));
|
|
9573
|
+
if (missing.length === 0) {
|
|
9574
|
+
finish();
|
|
9575
|
+
return;
|
|
9576
|
+
}
|
|
9577
|
+
Promise.all(missing.map((paperId) => fetchPaper(taskId, paperId).then((loaded) => {
|
|
9578
|
+
known.set(paperId, loaded.paper);
|
|
9579
|
+
}).catch(() => void 0))).then(finish);
|
|
9315
9580
|
};
|
|
9316
9581
|
/** 把任务 id 与当前条件带入会话草稿,新会话也能续接同一任务。 */
|
|
9317
9582
|
const insertTask = () => {
|
|
9318
9583
|
if (detail === null) return;
|
|
9319
9584
|
const outcome = insertQuote(inputActions, taskText(t, detail), taskSummary(t, detail));
|
|
9320
|
-
if (outcome === "inserted")
|
|
9321
|
-
else
|
|
9585
|
+
if (outcome === "inserted") notify(t("task.inserted"));
|
|
9586
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
9322
9587
|
};
|
|
9323
9588
|
const downloadPaper = (paper) => {
|
|
9324
9589
|
if (taskId === null) return;
|
|
9590
|
+
const paperId = paper.paperId;
|
|
9591
|
+
setDownloads((previous) => ({
|
|
9592
|
+
...previous,
|
|
9593
|
+
[paperId]: { state: "downloading" }
|
|
9594
|
+
}));
|
|
9325
9595
|
(async () => {
|
|
9326
9596
|
try {
|
|
9327
|
-
const result = await downloadPaperPdf(taskId,
|
|
9328
|
-
if (result.status === "downloaded")
|
|
9329
|
-
|
|
9330
|
-
|
|
9597
|
+
const result = await downloadPaperPdf(taskId, paperId);
|
|
9598
|
+
if (result.status === "downloaded") {
|
|
9599
|
+
setDownloads((previous) => ({
|
|
9600
|
+
...previous,
|
|
9601
|
+
[paperId]: {
|
|
9602
|
+
state: "downloaded",
|
|
9603
|
+
...result.path === void 0 ? {} : { path: result.path }
|
|
9604
|
+
}
|
|
9605
|
+
}));
|
|
9606
|
+
notify(tpl(t("pdf.downloaded"), { name: result.filename ?? "" }), "info", 6e3);
|
|
9607
|
+
} else {
|
|
9608
|
+
setDownloads((previous) => ({
|
|
9609
|
+
...previous,
|
|
9610
|
+
[paperId]: {
|
|
9611
|
+
state: "failed",
|
|
9612
|
+
error: result.error ?? result.status,
|
|
9613
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
9614
|
+
}
|
|
9615
|
+
}));
|
|
9616
|
+
notify(result.status === "html-only" ? tpl(t("pdf.htmlFallback"), { message: result.error ?? "" }) : tpl(t("pdf.error"), { message: result.error ?? result.status }), "error", 8e3);
|
|
9617
|
+
}
|
|
9331
9618
|
} catch (error) {
|
|
9332
|
-
|
|
9619
|
+
setDownloads((previous) => ({
|
|
9620
|
+
...previous,
|
|
9621
|
+
[paperId]: {
|
|
9622
|
+
state: "failed",
|
|
9623
|
+
error: messageOf(error)
|
|
9624
|
+
}
|
|
9625
|
+
}));
|
|
9626
|
+
notify(tpl(t("pdf.error"), { message: messageOf(error) }), "error", 8e3);
|
|
9333
9627
|
} finally {
|
|
9334
9628
|
view.reload();
|
|
9335
9629
|
}
|
|
@@ -9338,14 +9632,14 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9338
9632
|
const zoteroPaper = (paper) => {
|
|
9339
9633
|
if (taskId === null) return;
|
|
9340
9634
|
syncZotero(taskId, [paper.paperId]).then((result) => {
|
|
9341
|
-
|
|
9635
|
+
notify(zoteroToast(t, result.saved, result.failed, result.messages));
|
|
9342
9636
|
if (result.failed > 0) setRisFallback({
|
|
9343
9637
|
ids: [paper.paperId],
|
|
9344
9638
|
message: result.messages[0] ?? t("pdf.zoteroFailed")
|
|
9345
9639
|
});
|
|
9346
9640
|
}).catch((error) => {
|
|
9347
9641
|
const message = messageOf(error);
|
|
9348
|
-
|
|
9642
|
+
notify(tpl(t("pdf.zoteroFailed"), { message }));
|
|
9349
9643
|
setRisFallback({
|
|
9350
9644
|
ids: [paper.paperId],
|
|
9351
9645
|
message
|
|
@@ -9360,7 +9654,17 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9360
9654
|
return next;
|
|
9361
9655
|
});
|
|
9362
9656
|
};
|
|
9363
|
-
|
|
9657
|
+
/** 本页全选用并集:翻页可累加,不会把其它页已选的文献冲掉。 */
|
|
9658
|
+
const selectPage = (pageIds) => {
|
|
9659
|
+
if (pageIds.length === 0) return;
|
|
9660
|
+
setSelected((previous) => {
|
|
9661
|
+
const next = new Set(previous);
|
|
9662
|
+
for (const paperId of pageIds) next.add(paperId);
|
|
9663
|
+
return next;
|
|
9664
|
+
});
|
|
9665
|
+
};
|
|
9666
|
+
/** 取消全选:清空所有已选(含其它页)。 */
|
|
9667
|
+
const clearSelection = () => setSelected(/* @__PURE__ */ new Set());
|
|
9364
9668
|
const annotateMany = (patch) => {
|
|
9365
9669
|
if (ids.length === 0) return;
|
|
9366
9670
|
view.annotate(ids.map((paperId) => ({
|
|
@@ -9380,44 +9684,129 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9380
9684
|
})));
|
|
9381
9685
|
})();
|
|
9382
9686
|
};
|
|
9383
|
-
/**
|
|
9384
|
-
|
|
9385
|
-
|
|
9687
|
+
/**
|
|
9688
|
+
* 批量下载:打开进度弹窗后按最多 5 篇并发下载。
|
|
9689
|
+
*
|
|
9690
|
+
* 已下载(含文件已删除以外的正常已下载)的文献直接略过,不再重复请求;
|
|
9691
|
+
* 每篇的结果实时写入 downloads,弹窗只做集中展示。批量未结束时再次触发
|
|
9692
|
+
* 只重新打开弹窗,避免两轮循环重复下载同一批文献。
|
|
9693
|
+
*/
|
|
9694
|
+
const startBatchDownload = (entries) => {
|
|
9695
|
+
if (taskId === null || entries.length === 0) return;
|
|
9696
|
+
if (batchRunningRef.current) {
|
|
9697
|
+
setBatchOpen(true);
|
|
9698
|
+
return;
|
|
9699
|
+
}
|
|
9700
|
+
setBatchItems(entries.map((entry) => ({ ...entry })));
|
|
9701
|
+
setBatchOpen(true);
|
|
9702
|
+
batchRunningRef.current = true;
|
|
9703
|
+
setBatchRunning(true);
|
|
9386
9704
|
(async () => {
|
|
9387
9705
|
let ok = 0;
|
|
9388
|
-
let
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9706
|
+
let manualCount = 0;
|
|
9707
|
+
let failedCount = 0;
|
|
9708
|
+
let changed = false;
|
|
9709
|
+
/** 单篇:跳过已下载的,下载结果实时写回 downloads。 */
|
|
9710
|
+
const runOne = async (paperId) => {
|
|
9711
|
+
const runtime = downloadsRef.current[paperId];
|
|
9712
|
+
if (runtime?.state === "downloading") return;
|
|
9713
|
+
if (runtime?.state === "downloaded" || statesRef.current[paperId]?.pdfStatus === "downloaded") return;
|
|
9714
|
+
changed = true;
|
|
9715
|
+
setDownloads((previous) => ({
|
|
9716
|
+
...previous,
|
|
9717
|
+
[paperId]: { state: "downloading" }
|
|
9718
|
+
}));
|
|
9719
|
+
try {
|
|
9720
|
+
const result = await downloadPaperPdf(taskId, paperId);
|
|
9721
|
+
if (result.status === "downloaded") {
|
|
9722
|
+
ok += 1;
|
|
9723
|
+
setDownloads((previous) => ({
|
|
9724
|
+
...previous,
|
|
9725
|
+
[paperId]: {
|
|
9726
|
+
state: "downloaded",
|
|
9727
|
+
...result.path === void 0 ? {} : { path: result.path }
|
|
9728
|
+
}
|
|
9729
|
+
}));
|
|
9730
|
+
} else {
|
|
9731
|
+
if (isManualPdfFailure(result.reason)) manualCount += 1;
|
|
9732
|
+
else failedCount += 1;
|
|
9733
|
+
setDownloads((previous) => ({
|
|
9734
|
+
...previous,
|
|
9735
|
+
[paperId]: {
|
|
9736
|
+
state: "failed",
|
|
9737
|
+
error: result.error ?? result.status,
|
|
9738
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
9739
|
+
}
|
|
9740
|
+
}));
|
|
9741
|
+
}
|
|
9742
|
+
} catch (error) {
|
|
9743
|
+
failedCount += 1;
|
|
9744
|
+
setDownloads((previous) => ({
|
|
9745
|
+
...previous,
|
|
9746
|
+
[paperId]: {
|
|
9747
|
+
state: "failed",
|
|
9748
|
+
error: messageOf(error)
|
|
9749
|
+
}
|
|
9750
|
+
}));
|
|
9751
|
+
}
|
|
9752
|
+
};
|
|
9753
|
+
let cursor = 0;
|
|
9754
|
+
const worker = async () => {
|
|
9755
|
+
while (cursor < entries.length) {
|
|
9756
|
+
const entry = entries[cursor];
|
|
9757
|
+
cursor += 1;
|
|
9758
|
+
if (entry !== void 0) await runOne(entry.paperId);
|
|
9759
|
+
}
|
|
9760
|
+
};
|
|
9761
|
+
try {
|
|
9762
|
+
await Promise.all(Array.from({ length: Math.min(BATCH_CONCURRENCY, entries.length) }, () => worker()));
|
|
9763
|
+
} finally {
|
|
9764
|
+
batchRunningRef.current = false;
|
|
9765
|
+
setBatchRunning(false);
|
|
9766
|
+
if (changed) view.reload();
|
|
9395
9767
|
}
|
|
9396
|
-
|
|
9768
|
+
if (changed) notify(tpl(t("bulk.downloadDone"), {
|
|
9397
9769
|
ok,
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9770
|
+
manual: manualCount,
|
|
9771
|
+
failed: failedCount
|
|
9772
|
+
}), failedCount > 0 ? "error" : "info", 8e3);
|
|
9401
9773
|
})();
|
|
9402
9774
|
};
|
|
9775
|
+
/** 结果页选中项:标题优先取当前页,跨页选中的文献再异步补详情。 */
|
|
9776
|
+
const downloadMany = () => {
|
|
9777
|
+
if (taskId === null || ids.length === 0) return;
|
|
9778
|
+
const known = /* @__PURE__ */ new Map();
|
|
9779
|
+
for (const paper of view.page?.papers ?? []) known.set(paper.paperId, paper.title);
|
|
9780
|
+
startBatchDownload(ids.map((paperId) => ({
|
|
9781
|
+
paperId,
|
|
9782
|
+
title: known.get(paperId) ?? ""
|
|
9783
|
+
})));
|
|
9784
|
+
const missing = ids.filter((paperId) => !known.has(paperId));
|
|
9785
|
+
if (missing.length === 0) return;
|
|
9786
|
+
Promise.all(missing.map((paperId) => fetchPaper(taskId, paperId).then((loaded) => {
|
|
9787
|
+
setBatchItems((previous) => previous.map((item) => item.paperId === paperId ? {
|
|
9788
|
+
...item,
|
|
9789
|
+
title: loaded.paper.title
|
|
9790
|
+
} : item));
|
|
9791
|
+
}).catch(() => void 0)));
|
|
9792
|
+
};
|
|
9403
9793
|
const zoteroIds = (paperIds) => {
|
|
9404
9794
|
if (taskId === null || paperIds.length === 0) return;
|
|
9405
9795
|
syncZotero(taskId, paperIds).then((result) => {
|
|
9406
|
-
|
|
9796
|
+
notify(zoteroToast(t, result.saved, result.failed, result.messages));
|
|
9407
9797
|
if (result.failed > 0) setRisFallback({
|
|
9408
9798
|
ids: [...paperIds],
|
|
9409
9799
|
message: result.messages[0] ?? t("pdf.zoteroFailed")
|
|
9410
9800
|
});
|
|
9411
9801
|
}).catch((error) => {
|
|
9412
9802
|
const message = messageOf(error);
|
|
9413
|
-
|
|
9803
|
+
notify(tpl(t("pdf.zoteroFailed"), { message }));
|
|
9414
9804
|
setRisFallback({
|
|
9415
9805
|
ids: [...paperIds],
|
|
9416
9806
|
message
|
|
9417
9807
|
});
|
|
9418
9808
|
});
|
|
9419
9809
|
};
|
|
9420
|
-
const downloadMany = () => downloadIds(ids);
|
|
9421
9810
|
const zoteroMany = () => zoteroIds(ids);
|
|
9422
9811
|
if (taskId === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9423
9812
|
style: panelStyle,
|
|
@@ -9447,11 +9836,12 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9447
9836
|
const failed = detail.result.sources.filter((source) => !source.ok && !source.skipped);
|
|
9448
9837
|
const skipped = detail.result.sources.filter((source) => source.skipped);
|
|
9449
9838
|
const page = view.page;
|
|
9839
|
+
const pageIds = page?.papers.map((paper) => paper.paperId) ?? [];
|
|
9450
9840
|
/** 当前生效的排序:面板选择优先,未选择时跟随任务条件。 */
|
|
9451
9841
|
const sortValue = filters.sort ?? detail.conditions.sort;
|
|
9452
9842
|
const coverage = overview?.fullText;
|
|
9453
9843
|
const overviewAvailable = coverage === void 0 ? 0 : coverage.available + coverage.downloaded + coverage.htmlOnly;
|
|
9454
|
-
const overviewMissing = coverage === void 0 ? 0 : coverage.unknown + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
9844
|
+
const overviewMissing = coverage === void 0 ? 0 : coverage.unknown + coverage.missing + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
9455
9845
|
const overviewRate = overview === null || overview.loaded === 0 ? 0 : Math.round(overviewAvailable / overview.loaded * 100);
|
|
9456
9846
|
const conditionMeta = [tpl(t("task.pageSize"), { n: detail.conditions.pageSize })];
|
|
9457
9847
|
if (detail.conditions.yearFrom !== void 0 || detail.conditions.yearTo !== void 0) conditionMeta.push(tpl(t("task.conditionYear"), { value: `${detail.conditions.yearFrom ?? ""}–${detail.conditions.yearTo ?? ""}` }));
|
|
@@ -9723,34 +10113,40 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9723
10113
|
onCancel: () => setEditingConditions(false),
|
|
9724
10114
|
onSaved: () => {
|
|
9725
10115
|
setEditingConditions(false);
|
|
9726
|
-
|
|
10116
|
+
notify(t("condition.saved"));
|
|
9727
10117
|
view.reload();
|
|
9728
10118
|
}
|
|
9729
10119
|
}),
|
|
9730
|
-
toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
9731
|
-
style:
|
|
9732
|
-
children: toast
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
10120
|
+
(toast !== "" || risFallback !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10121
|
+
style: toastStackStyle,
|
|
10122
|
+
children: [toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10123
|
+
role: "status",
|
|
10124
|
+
style: {
|
|
10125
|
+
...toastStyle,
|
|
10126
|
+
...toastTone === "error" ? toastErrorStyle : null
|
|
10127
|
+
},
|
|
10128
|
+
children: toast
|
|
10129
|
+
}), risFallback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10130
|
+
style: RIS_FALLBACK_STYLE,
|
|
10131
|
+
children: [
|
|
10132
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10133
|
+
style: { minWidth: 0 },
|
|
10134
|
+
children: tpl(t("pdf.zoteroFallback"), { message: risFallback.message })
|
|
10135
|
+
}),
|
|
10136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
10137
|
+
href: exportUrl(taskId, "ris", {}, risFallback.ids),
|
|
10138
|
+
download: true,
|
|
10139
|
+
style: RIS_FALLBACK_LINK_STYLE,
|
|
10140
|
+
children: t("pdf.zoteroFallbackLink")
|
|
10141
|
+
}),
|
|
10142
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10143
|
+
type: "button",
|
|
10144
|
+
style: RIS_FALLBACK_BUTTON_STYLE,
|
|
10145
|
+
onClick: () => setRisFallback(null),
|
|
10146
|
+
children: t("pdf.zoteroFallbackDismiss")
|
|
10147
|
+
})
|
|
10148
|
+
]
|
|
10149
|
+
})]
|
|
9754
10150
|
}),
|
|
9755
10151
|
view.error !== null && detail.result.totalLoaded > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9756
10152
|
style: {
|
|
@@ -9846,23 +10242,48 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9846
10242
|
}),
|
|
9847
10243
|
page !== null && page.total > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9848
10244
|
style: PAGE_TOOLBAR_ROW_STYLE,
|
|
9849
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
9850
|
-
style:
|
|
9851
|
-
children: [
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
10245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10246
|
+
style: PAGE_TOOLBAR_LEFT_STYLE,
|
|
10247
|
+
children: [
|
|
10248
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
10249
|
+
style: PAGE_SORT_LABEL_STYLE,
|
|
10250
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("filter.sort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
10251
|
+
style: PAGE_SORT_SELECT_STYLE,
|
|
10252
|
+
value: sortValue,
|
|
10253
|
+
onChange: (event) => {
|
|
10254
|
+
const next = event.target.value;
|
|
10255
|
+
setFilters((previous) => ({
|
|
10256
|
+
...previous,
|
|
10257
|
+
sort: next === detail.conditions.sort ? void 0 : next
|
|
10258
|
+
}));
|
|
10259
|
+
},
|
|
10260
|
+
children: SORT_MODES.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
10261
|
+
value: mode,
|
|
10262
|
+
children: t(`sort.${mode}`)
|
|
10263
|
+
}, mode))
|
|
10264
|
+
})]
|
|
10265
|
+
}),
|
|
10266
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10267
|
+
type: "button",
|
|
10268
|
+
style: PAGE_TOOLBAR_ACTION_STYLE,
|
|
10269
|
+
title: t("bulk.selectPageHint"),
|
|
10270
|
+
onClick: () => selectPage(pageIds),
|
|
10271
|
+
children: t("bulk.selectPage")
|
|
10272
|
+
}),
|
|
10273
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10274
|
+
type: "button",
|
|
10275
|
+
style: selected.size === 0 ? PAGE_TOOLBAR_ACTION_DISABLED_STYLE : PAGE_TOOLBAR_ACTION_STYLE,
|
|
10276
|
+
disabled: selected.size === 0,
|
|
10277
|
+
title: t("bulk.unselectAllHint"),
|
|
10278
|
+
onClick: clearSelection,
|
|
10279
|
+
children: t("bulk.unselectAll")
|
|
10280
|
+
}),
|
|
10281
|
+
selected.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10282
|
+
style: PAGE_TOOLBAR_COUNT_STYLE,
|
|
10283
|
+
title: t("bulk.keepHint"),
|
|
10284
|
+
children: tpl(t("bulk.count"), { n: selected.size })
|
|
10285
|
+
})
|
|
10286
|
+
]
|
|
9866
10287
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pager, {
|
|
9867
10288
|
t,
|
|
9868
10289
|
index: view.pageIndex,
|
|
@@ -9876,16 +10297,21 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9876
10297
|
taskId,
|
|
9877
10298
|
count: selected.size,
|
|
9878
10299
|
ids,
|
|
9879
|
-
pageIds
|
|
10300
|
+
pageIds,
|
|
9880
10301
|
groups: detail.groups,
|
|
9881
10302
|
busy: view.busy,
|
|
9882
10303
|
filters,
|
|
9883
|
-
onClear:
|
|
9884
|
-
onSelectPage:
|
|
10304
|
+
onClear: clearSelection,
|
|
10305
|
+
onSelectPage: selectPage,
|
|
9885
10306
|
onAnnotate: annotateMany,
|
|
9886
10307
|
onInsert: insertMany,
|
|
9887
10308
|
onDownload: downloadMany,
|
|
9888
|
-
onZotero: zoteroMany
|
|
10309
|
+
onZotero: zoteroMany,
|
|
10310
|
+
batchRunning,
|
|
10311
|
+
...detail.downloadDir === void 0 || onRevealLocal === void 0 ? {} : {
|
|
10312
|
+
downloadDir: detail.downloadDir,
|
|
10313
|
+
onRevealDir: onRevealLocal
|
|
10314
|
+
}
|
|
9889
10315
|
}),
|
|
9890
10316
|
page !== null && page.papers.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
9891
10317
|
style: listStyle$1,
|
|
@@ -9896,6 +10322,10 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9896
10322
|
state: detail.states[paper.paperId],
|
|
9897
10323
|
groups: detail.groups,
|
|
9898
10324
|
busy: view.busy,
|
|
10325
|
+
...detail.downloadDir === void 0 ? {} : { downloadDir: detail.downloadDir },
|
|
10326
|
+
...downloads[paper.paperId] === void 0 ? {} : { download: downloads[paper.paperId] },
|
|
10327
|
+
...onOpenLocal === void 0 ? {} : { onOpenLocal },
|
|
10328
|
+
...onRevealLocal === void 0 ? {} : { onRevealLocal },
|
|
9899
10329
|
selected: selected.has(paper.paperId),
|
|
9900
10330
|
onToggleSelect: toggleSelect,
|
|
9901
10331
|
onInsert: insert,
|
|
@@ -9946,8 +10376,13 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9946
10376
|
states: detail.states,
|
|
9947
10377
|
stats: overview,
|
|
9948
10378
|
onDownload: downloadPaper,
|
|
10379
|
+
...onOpenLocal === void 0 ? {} : { onOpenLocal },
|
|
10380
|
+
...onRevealLocal === void 0 ? {} : { onRevealLocal },
|
|
9949
10381
|
onZotero: zoteroPaper,
|
|
9950
|
-
onBatchDownload: (papers) =>
|
|
10382
|
+
onBatchDownload: (papers) => startBatchDownload(papers.map((paper) => ({
|
|
10383
|
+
paperId: paper.paperId,
|
|
10384
|
+
title: paper.title
|
|
10385
|
+
}))),
|
|
9951
10386
|
onBatchZotero: (papers) => zoteroIds(papers.map((paper) => paper.paperId)),
|
|
9952
10387
|
onShowInResults: (viewKey) => {
|
|
9953
10388
|
setFilters(viewKey === "missing" ? { missingFullText: true } : { pdfStatus: viewKey });
|
|
@@ -9958,6 +10393,7 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9958
10393
|
taskId,
|
|
9959
10394
|
detail,
|
|
9960
10395
|
onSaved: view.reload,
|
|
10396
|
+
onNotify: notify,
|
|
9961
10397
|
onPickGroup: (groupId) => {
|
|
9962
10398
|
setFilters((previous) => ({
|
|
9963
10399
|
...previous,
|
|
@@ -9969,7 +10405,8 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9969
10405
|
t,
|
|
9970
10406
|
taskId,
|
|
9971
10407
|
detail,
|
|
9972
|
-
onSaved: view.reload
|
|
10408
|
+
onSaved: view.reload,
|
|
10409
|
+
onNotify: notify
|
|
9973
10410
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompareTab, {
|
|
9974
10411
|
t,
|
|
9975
10412
|
taskId,
|
|
@@ -9986,6 +10423,17 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9986
10423
|
}
|
|
9987
10424
|
})
|
|
9988
10425
|
}),
|
|
10426
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadDialog, {
|
|
10427
|
+
t,
|
|
10428
|
+
open: batchOpen,
|
|
10429
|
+
items: batchItems,
|
|
10430
|
+
states: detail.states,
|
|
10431
|
+
downloads,
|
|
10432
|
+
onClose: () => {
|
|
10433
|
+
setBatchOpen(false);
|
|
10434
|
+
view.reload();
|
|
10435
|
+
}
|
|
10436
|
+
}),
|
|
9989
10437
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundsDialog, {
|
|
9990
10438
|
t,
|
|
9991
10439
|
open: showRounds,
|
|
@@ -9995,7 +10443,7 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9995
10443
|
busy: active,
|
|
9996
10444
|
onRerun: () => {
|
|
9997
10445
|
view.reload();
|
|
9998
|
-
|
|
10446
|
+
notify(t("rounds.rerunStarted"));
|
|
9999
10447
|
}
|
|
10000
10448
|
}),
|
|
10001
10449
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskListDialog, {
|
|
@@ -10154,6 +10602,8 @@ const EXPORT_FORMATS = [
|
|
|
10154
10602
|
"bibtex",
|
|
10155
10603
|
"md"
|
|
10156
10604
|
];
|
|
10605
|
+
/** 批量下载的并发上限:同时下载 5 篇。 */
|
|
10606
|
+
const BATCH_CONCURRENCY = 5;
|
|
10157
10607
|
/** 筛读计数(点击筛选):与「筛选」按钮同排,接在筛选条最前面。 */
|
|
10158
10608
|
function ScreeningPills({ t, detail, filters, onChange }) {
|
|
10159
10609
|
const counts = detail.stats.screening;
|
|
@@ -10186,7 +10636,7 @@ function ScreeningPills({ t, detail, filters, onChange }) {
|
|
|
10186
10636
|
});
|
|
10187
10637
|
}
|
|
10188
10638
|
/** 选中多篇后的批量操作条:筛读、分组、下载、Zotero、按选中导出。 */
|
|
10189
|
-
function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, filters, onClear, onSelectPage, onAnnotate, onInsert, onDownload, onZotero }) {
|
|
10639
|
+
function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, batchRunning, downloadDir, onRevealDir, filters, onClear, onSelectPage, onAnnotate, onInsert, onDownload, onZotero }) {
|
|
10190
10640
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10191
10641
|
style: BULK_STYLE,
|
|
10192
10642
|
children: [
|
|
@@ -10261,11 +10711,15 @@ function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, filters, onClea
|
|
|
10261
10711
|
label: t("bulk.insert"),
|
|
10262
10712
|
onClick: onInsert
|
|
10263
10713
|
}),
|
|
10264
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
10265
|
-
|
|
10714
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkDownloadButton, {
|
|
10715
|
+
t,
|
|
10266
10716
|
label: t("bulk.download"),
|
|
10267
10717
|
onClick: onDownload,
|
|
10268
|
-
disabled: busy
|
|
10718
|
+
disabled: busy || batchRunning,
|
|
10719
|
+
...downloadDir === void 0 || onRevealDir === void 0 ? {} : {
|
|
10720
|
+
downloadDir,
|
|
10721
|
+
onReveal: onRevealDir
|
|
10722
|
+
}
|
|
10269
10723
|
}),
|
|
10270
10724
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkActionButton, {
|
|
10271
10725
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoteroIcon, {}),
|
|
@@ -10317,6 +10771,73 @@ function BulkActionButton({ icon, label, onClick, disabled = false }) {
|
|
|
10317
10771
|
children: [icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
10318
10772
|
});
|
|
10319
10773
|
}
|
|
10774
|
+
/**
|
|
10775
|
+
* 批量下载按钮 + 下三角:点主体打开下载进度弹窗;点箭头展开菜单,
|
|
10776
|
+
* 在系统文件管理器中打开下载目录(与条目上「查看已下载 ▾」的菜单一致)。
|
|
10777
|
+
*/
|
|
10778
|
+
function BulkDownloadButton({ t, label, onClick, disabled, downloadDir, onReveal }) {
|
|
10779
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
10780
|
+
const groupRef = (0, react.useRef)(null);
|
|
10781
|
+
(0, react.useEffect)(() => {
|
|
10782
|
+
if (!open) return void 0;
|
|
10783
|
+
const closeOnOutside = (event) => {
|
|
10784
|
+
if (groupRef.current !== null && !groupRef.current.contains(event.target)) setOpen(false);
|
|
10785
|
+
};
|
|
10786
|
+
const closeOnEscape = (event) => {
|
|
10787
|
+
if (event.key === "Escape") setOpen(false);
|
|
10788
|
+
};
|
|
10789
|
+
document.addEventListener("mousedown", closeOnOutside);
|
|
10790
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
10791
|
+
return () => {
|
|
10792
|
+
document.removeEventListener("mousedown", closeOnOutside);
|
|
10793
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
10794
|
+
};
|
|
10795
|
+
}, [open]);
|
|
10796
|
+
const revealable = downloadDir !== void 0 && onReveal !== void 0;
|
|
10797
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
10798
|
+
ref: groupRef,
|
|
10799
|
+
style: BULK_DOWNLOAD_GROUP_STYLE,
|
|
10800
|
+
children: [
|
|
10801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkActionButton, {
|
|
10802
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, {}),
|
|
10803
|
+
label,
|
|
10804
|
+
onClick,
|
|
10805
|
+
disabled
|
|
10806
|
+
}),
|
|
10807
|
+
revealable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10808
|
+
type: "button",
|
|
10809
|
+
style: {
|
|
10810
|
+
...BULK_ARROW_STYLE,
|
|
10811
|
+
...disabled ? {
|
|
10812
|
+
opacity: .45,
|
|
10813
|
+
cursor: "default"
|
|
10814
|
+
} : null
|
|
10815
|
+
},
|
|
10816
|
+
"aria-haspopup": "menu",
|
|
10817
|
+
"aria-expanded": open,
|
|
10818
|
+
"aria-label": t("bulk.revealDir"),
|
|
10819
|
+
title: t("bulk.revealDir"),
|
|
10820
|
+
disabled,
|
|
10821
|
+
onClick: () => setOpen((value) => !value),
|
|
10822
|
+
children: "▾"
|
|
10823
|
+
}),
|
|
10824
|
+
open && revealable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10825
|
+
role: "menu",
|
|
10826
|
+
style: BULK_MENU_STYLE,
|
|
10827
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10828
|
+
type: "button",
|
|
10829
|
+
role: "menuitem",
|
|
10830
|
+
style: BULK_MENU_ITEM_STYLE,
|
|
10831
|
+
onClick: () => {
|
|
10832
|
+
setOpen(false);
|
|
10833
|
+
if (downloadDir !== void 0) onReveal?.(downloadDir);
|
|
10834
|
+
},
|
|
10835
|
+
children: t("bulk.revealDir")
|
|
10836
|
+
})
|
|
10837
|
+
})
|
|
10838
|
+
]
|
|
10839
|
+
});
|
|
10840
|
+
}
|
|
10320
10841
|
/** 结果页翻页条:首页 / 上一页 / 页码 / 下一页 / 末页。单页时不渲染。 */
|
|
10321
10842
|
function Pager({ t, index, count, total, onChange }) {
|
|
10322
10843
|
if (count <= 1) return null;
|
|
@@ -10399,16 +10920,23 @@ const ROUND_RESULTS_ICON_STYLE = {
|
|
|
10399
10920
|
flexShrink: 0
|
|
10400
10921
|
};
|
|
10401
10922
|
const RIS_FALLBACK_STYLE = {
|
|
10923
|
+
...toastStyle,
|
|
10924
|
+
pointerEvents: "auto",
|
|
10402
10925
|
display: "flex",
|
|
10403
10926
|
alignItems: "center",
|
|
10404
10927
|
flexWrap: "wrap",
|
|
10405
10928
|
gap: 8,
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
fontSize:
|
|
10411
|
-
|
|
10929
|
+
borderLeft: "3px solid #b26a00"
|
|
10930
|
+
};
|
|
10931
|
+
const RIS_FALLBACK_LINK_STYLE = {
|
|
10932
|
+
color: "#fff",
|
|
10933
|
+
fontSize: 12,
|
|
10934
|
+
textDecoration: "underline"
|
|
10935
|
+
};
|
|
10936
|
+
const RIS_FALLBACK_BUTTON_STYLE = {
|
|
10937
|
+
...linkButtonStyle,
|
|
10938
|
+
color: "#fff",
|
|
10939
|
+
textDecoration: "underline"
|
|
10412
10940
|
};
|
|
10413
10941
|
const TAB_ROW_STYLE = {
|
|
10414
10942
|
display: "flex",
|
|
@@ -10517,6 +11045,50 @@ const BULK_BUTTON_STYLE = {
|
|
|
10517
11045
|
lineHeight: 1.4,
|
|
10518
11046
|
cursor: "pointer"
|
|
10519
11047
|
};
|
|
11048
|
+
/** 批量下载 + 下三角的组合:箭头紧贴按钮右侧,菜单相对整体定位。 */
|
|
11049
|
+
const BULK_DOWNLOAD_GROUP_STYLE = {
|
|
11050
|
+
position: "relative",
|
|
11051
|
+
display: "inline-flex",
|
|
11052
|
+
alignItems: "center",
|
|
11053
|
+
gap: 2
|
|
11054
|
+
};
|
|
11055
|
+
const BULK_ARROW_STYLE = {
|
|
11056
|
+
padding: "2px 3px",
|
|
11057
|
+
borderRadius: 5,
|
|
11058
|
+
border: "1px solid rgba(80,140,255,0.4)",
|
|
11059
|
+
background: "transparent",
|
|
11060
|
+
color: ACCENT,
|
|
11061
|
+
fontSize: 12,
|
|
11062
|
+
lineHeight: 1.4,
|
|
11063
|
+
cursor: "pointer"
|
|
11064
|
+
};
|
|
11065
|
+
const BULK_MENU_STYLE = {
|
|
11066
|
+
position: "absolute",
|
|
11067
|
+
top: "100%",
|
|
11068
|
+
left: 0,
|
|
11069
|
+
marginTop: 3,
|
|
11070
|
+
zIndex: 40,
|
|
11071
|
+
display: "flex",
|
|
11072
|
+
flexDirection: "column",
|
|
11073
|
+
minWidth: 132,
|
|
11074
|
+
padding: 2,
|
|
11075
|
+
borderRadius: 6,
|
|
11076
|
+
border: "1px solid rgba(127,127,127,0.3)",
|
|
11077
|
+
background: "#fff",
|
|
11078
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.18)"
|
|
11079
|
+
};
|
|
11080
|
+
const BULK_MENU_ITEM_STYLE = {
|
|
11081
|
+
padding: "5px 8px",
|
|
11082
|
+
border: "none",
|
|
11083
|
+
borderRadius: 4,
|
|
11084
|
+
background: "transparent",
|
|
11085
|
+
color: "#1f2328",
|
|
11086
|
+
fontSize: 12,
|
|
11087
|
+
lineHeight: 1.4,
|
|
11088
|
+
textAlign: "left",
|
|
11089
|
+
whiteSpace: "nowrap",
|
|
11090
|
+
cursor: "pointer"
|
|
11091
|
+
};
|
|
10520
11092
|
const COVERAGE_WRAP_STYLE = {
|
|
10521
11093
|
display: "flex",
|
|
10522
11094
|
flexDirection: "column",
|
|
@@ -10550,6 +11122,7 @@ const QUICK_FILTER_ACTIVE_STYLE = {
|
|
|
10550
11122
|
};
|
|
10551
11123
|
const TASK_PANEL_STYLE = {
|
|
10552
11124
|
...panelStyle,
|
|
11125
|
+
position: "relative",
|
|
10553
11126
|
height: "100%",
|
|
10554
11127
|
minHeight: 0,
|
|
10555
11128
|
boxSizing: "border-box"
|
|
@@ -10563,7 +11136,7 @@ const SCROLL_BODY_STYLE = {
|
|
|
10563
11136
|
overflowY: "auto",
|
|
10564
11137
|
overflowX: "hidden"
|
|
10565
11138
|
};
|
|
10566
|
-
/**
|
|
11139
|
+
/** 排序(含本页全选 / 取消全选)与翻页同一行:操作靠左,翻页(含总篇数)靠右。 */
|
|
10567
11140
|
const PAGE_TOOLBAR_ROW_STYLE = {
|
|
10568
11141
|
display: "flex",
|
|
10569
11142
|
alignItems: "center",
|
|
@@ -10572,6 +11145,27 @@ const PAGE_TOOLBAR_ROW_STYLE = {
|
|
|
10572
11145
|
gap: 8,
|
|
10573
11146
|
padding: "2px 0"
|
|
10574
11147
|
};
|
|
11148
|
+
const PAGE_TOOLBAR_LEFT_STYLE = {
|
|
11149
|
+
display: "flex",
|
|
11150
|
+
alignItems: "center",
|
|
11151
|
+
flexWrap: "wrap",
|
|
11152
|
+
gap: 8
|
|
11153
|
+
};
|
|
11154
|
+
const PAGE_TOOLBAR_ACTION_STYLE = {
|
|
11155
|
+
...linkButtonStyle,
|
|
11156
|
+
fontSize: 11.5,
|
|
11157
|
+
whiteSpace: "nowrap"
|
|
11158
|
+
};
|
|
11159
|
+
const PAGE_TOOLBAR_ACTION_DISABLED_STYLE = {
|
|
11160
|
+
...PAGE_TOOLBAR_ACTION_STYLE,
|
|
11161
|
+
opacity: .4,
|
|
11162
|
+
cursor: "default"
|
|
11163
|
+
};
|
|
11164
|
+
const PAGE_TOOLBAR_COUNT_STYLE = {
|
|
11165
|
+
fontSize: 11.5,
|
|
11166
|
+
opacity: .66,
|
|
11167
|
+
whiteSpace: "nowrap"
|
|
11168
|
+
};
|
|
10575
11169
|
const PAGE_SORT_LABEL_STYLE = {
|
|
10576
11170
|
display: "flex",
|
|
10577
11171
|
alignItems: "center",
|
|
@@ -10636,6 +11230,12 @@ const PAPER_TAB_ID = "litsearch-paper";
|
|
|
10636
11230
|
/** 设置对话框里这一页的 section id。 */
|
|
10637
11231
|
const SETTINGS_SECTION_ID = "litsearch";
|
|
10638
11232
|
const HEALTH_REFRESH_MS = 3e4;
|
|
11233
|
+
/** 文件资源地址:与 DSH 文档预览 tab 使用的 `dsh-resource://file/session/...` 保持一致。 */
|
|
11234
|
+
function localFileAddress(sessionId, path) {
|
|
11235
|
+
const encode = (segment) => encodeURIComponent(segment).replace(/%3A/gi, ":");
|
|
11236
|
+
const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
|
|
11237
|
+
return `dsh-resource://file/session/${encode(sessionId)}/${normalized.split("/").map(encode).join("/")}`;
|
|
11238
|
+
}
|
|
10639
11239
|
const name = "dsh-plugin-litsearch";
|
|
10640
11240
|
const inject = [
|
|
10641
11241
|
"slots",
|
|
@@ -10710,13 +11310,33 @@ function apply(ctx) {
|
|
|
10710
11310
|
const address = `${PAPER_ADDRESS_PREFIX}${encodeURIComponent(taskId)}/${encodeURIComponent(paperId)}`;
|
|
10711
11311
|
ctx.sidebarRight.openResource(address, title === "" ? void 0 : { params: { title } });
|
|
10712
11312
|
};
|
|
11313
|
+
/**
|
|
11314
|
+
* 在右侧栏用 DSH 文档预览打开已下载的本地 PDF。
|
|
11315
|
+
* 页面加载后文件可能被删除,先让 Host 复核一次;缺失时状态会广播为
|
|
11316
|
+
* `missing`,按钮随即变成「下载文件已删除点击重新下载」。
|
|
11317
|
+
*/
|
|
11318
|
+
const openPdfPreview = (taskId, paperId, path) => {
|
|
11319
|
+
recheckPaperPdf(taskId, paperId).then((result) => result.exists).catch(() => true).then((exists) => {
|
|
11320
|
+
if (!exists || path === "") return;
|
|
11321
|
+
const sessionId = ctx.sidebarRight.mounted.getSnapshot();
|
|
11322
|
+
if (sessionId === void 0) return;
|
|
11323
|
+
ctx.sidebarRight.openResource(localFileAddress(sessionId, path), { revealIfOpened: true });
|
|
11324
|
+
});
|
|
11325
|
+
};
|
|
11326
|
+
/** 在系统文件管理器中打开下载目录并选中该 PDF(失败时静默,避免打断阅读)。 */
|
|
11327
|
+
const revealPdf = (path) => {
|
|
11328
|
+
if (path === "") return;
|
|
11329
|
+
revealLocalFile(path).catch(() => void 0);
|
|
11330
|
+
};
|
|
10713
11331
|
const TaskBody = (props) => (0, react.createElement)(TaskPane, {
|
|
10714
11332
|
t,
|
|
10715
11333
|
useTabInfo: props.useTabInfo,
|
|
10716
11334
|
inputActions: props.inputActions,
|
|
10717
11335
|
onOpenPaper: openPaper,
|
|
10718
11336
|
onOpenTask: (id, title) => openTask(id, title),
|
|
10719
|
-
onCloseTask: (id) => closeTask(id)
|
|
11337
|
+
onCloseTask: (id) => closeTask(id),
|
|
11338
|
+
onOpenLocal: openPdfPreview,
|
|
11339
|
+
onRevealLocal: revealPdf
|
|
10720
11340
|
});
|
|
10721
11341
|
/** tab 标题:按地址里的任务 id 读缓存,缺了补取一次详情。 */
|
|
10722
11342
|
const TaskTitle = (props) => {
|
|
@@ -10750,7 +11370,9 @@ function apply(ctx) {
|
|
|
10750
11370
|
const PaperBody = (props) => (0, react.createElement)(PaperPane, {
|
|
10751
11371
|
t,
|
|
10752
11372
|
useTabInfo: props.useTabInfo,
|
|
10753
|
-
inputActions: props.inputActions
|
|
11373
|
+
inputActions: props.inputActions,
|
|
11374
|
+
onOpenLocal: openPdfPreview,
|
|
11375
|
+
onRevealLocal: revealPdf
|
|
10754
11376
|
});
|
|
10755
11377
|
const PaperTitle = (props) => {
|
|
10756
11378
|
const params = props.useTabInfo().tab.navigation?.params;
|