dsh-plugin-litsearch 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -9
- package/client/client.js +1096 -438
- package/lib/host/events.d.ts +3 -0
- package/lib/host/events.js +25 -1
- package/lib/host/paths.d.ts +43 -0
- package/lib/host/paths.js +128 -0
- package/lib/host/router.js +12 -0
- package/lib/host/tasks/filter.d.ts +2 -0
- package/lib/host/tasks/filter.js +4 -0
- package/lib/host/tasks/service.d.ts +22 -9
- package/lib/host/tasks/service.js +101 -30
- package/lib/host/tasks/store.d.ts +23 -3
- package/lib/host/tasks/store.js +63 -5
- package/lib/host/tools.d.ts +13 -6
- package/lib/host/tools.js +56 -22
- package/lib/search/config.d.ts +0 -7
- package/lib/search/config.js +0 -7
- package/lib/search/types.d.ts +10 -1
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -124,6 +124,7 @@ function resultParams(query) {
|
|
|
124
124
|
if (query.language !== void 0) params.set("language", query.language);
|
|
125
125
|
if (query.hasPdf === true) params.set("hasPdf", "1");
|
|
126
126
|
if (query.missingFullText === true) params.set("missingFullText", "1");
|
|
127
|
+
if (query.pdfStatus !== void 0) params.set("pdfStatus", query.pdfStatus);
|
|
127
128
|
if (query.groupId !== void 0 && query.groupId !== "") params.set("groupId", query.groupId);
|
|
128
129
|
if (query.venue !== void 0 && query.venue !== "") params.set("venue", query.venue);
|
|
129
130
|
if (query.author !== void 0 && query.author !== "") params.set("author", query.author);
|
|
@@ -249,6 +250,8 @@ function useHealth() {
|
|
|
249
250
|
*
|
|
250
251
|
* Host→Client 没有广播通道,这里按 `sinceRev` 轮询 `/events`:
|
|
251
252
|
* - 首次轮询只对齐版本号,不把宿主启动前的历史任务全打开;
|
|
253
|
+
* - 模型每次运行任务都会请求 `focus`:直接打开/聚焦该任务 tab(面板关着时
|
|
254
|
+
* 重新打开),不受「一轮只开一个」限制,但仍尊重自动打开开关;
|
|
252
255
|
* - 之后只对 `origin === 'model'` 的新任务调 `openResource`;
|
|
253
256
|
* - 同一轮(2 分钟)最多自动打开 1 个 tab,其余进「待打开」队列,由主面板横幅提示;
|
|
254
257
|
* - `maxTaskTabs` 按「最近真的打开过的 tab」计数(带 30 分钟有效期),不再用
|
|
@@ -359,6 +362,8 @@ function enqueuePending(task) {
|
|
|
359
362
|
const TAB_WINDOW_MS = 18e5;
|
|
360
363
|
/** 同一轮对话窗口:这段时间内最多自动打开 1 个 tab,其余进待打开队列。 */
|
|
361
364
|
const ROUND_WINDOW_MS = 12e4;
|
|
365
|
+
/** 聚焦请求的有效期:更早的请求可能是上次使用留下的,重新加载页面时不再弹出。 */
|
|
366
|
+
const FOCUS_WINDOW_MS = 6e5;
|
|
362
367
|
let opened = readOpened();
|
|
363
368
|
function readOpened() {
|
|
364
369
|
const stored = readJson(OPENED_KEY, []);
|
|
@@ -403,6 +408,9 @@ function activeTabCount() {
|
|
|
403
408
|
const cutoff = Date.now() - TAB_WINDOW_MS;
|
|
404
409
|
return opened.filter((entry) => entry.tab === true && entry.at >= cutoff).length;
|
|
405
410
|
}
|
|
411
|
+
function isFreshFocus(focus) {
|
|
412
|
+
return Date.now() - focus.at <= FOCUS_WINDOW_MS;
|
|
413
|
+
}
|
|
406
414
|
function lastAutoOpenAt() {
|
|
407
415
|
const cutoff = Date.now() - TAB_WINDOW_MS;
|
|
408
416
|
let last = 0;
|
|
@@ -452,9 +460,15 @@ function startAutoOpen(options) {
|
|
|
452
460
|
const first = !seeded;
|
|
453
461
|
seeded = true;
|
|
454
462
|
rev = page.rev;
|
|
455
|
-
if (first) return;
|
|
456
463
|
const settings = getHealth()?.settings;
|
|
457
|
-
|
|
464
|
+
const autoOpen = settings?.autoOpenTaskTab ?? true;
|
|
465
|
+
if (autoOpen && page.focus !== void 0 && isFreshFocus(page.focus)) {
|
|
466
|
+
const screen = options.sessionId?.();
|
|
467
|
+
const target = page.focus.sessionId;
|
|
468
|
+
if (target === void 0 || screen !== void 0 && target === screen) alsoFocus(page.focus.taskId, page.focus.title);
|
|
469
|
+
else rememberOpened(page.focus.taskId, false);
|
|
470
|
+
}
|
|
471
|
+
if (first || !autoOpen) return;
|
|
458
472
|
const maxTabs = settings?.maxTaskTabs ?? 5;
|
|
459
473
|
for (const task of page.tasks) {
|
|
460
474
|
if (task.origin !== "model") continue;
|
|
@@ -471,6 +485,11 @@ function startAutoOpen(options) {
|
|
|
471
485
|
options.open(task.id, task.title);
|
|
472
486
|
}
|
|
473
487
|
};
|
|
488
|
+
const alsoFocus = (id, title) => {
|
|
489
|
+
rememberTaskTitle(id, title);
|
|
490
|
+
rememberOpened(id, true);
|
|
491
|
+
options.open(id, title);
|
|
492
|
+
};
|
|
474
493
|
tick();
|
|
475
494
|
const timer = setInterval(() => {
|
|
476
495
|
tick();
|
|
@@ -545,6 +564,45 @@ function TaskListIcon() {
|
|
|
545
564
|
})]
|
|
546
565
|
});
|
|
547
566
|
}
|
|
567
|
+
/** 结果卡片字段图标统一尺寸:比右上角图标按钮略小,跟正文基线对齐。 */
|
|
568
|
+
const fieldSvgProps = {
|
|
569
|
+
...svgProps,
|
|
570
|
+
width: 14,
|
|
571
|
+
height: 14
|
|
572
|
+
};
|
|
573
|
+
/** 文献标题:带折角的文档 + 标题行。 */
|
|
574
|
+
function TitleIcon() {
|
|
575
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
576
|
+
...fieldSvgProps,
|
|
577
|
+
children: [
|
|
578
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
|
|
579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 3v5h5" }),
|
|
580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 13h8M8 17h5" })
|
|
581
|
+
]
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/** 作者:双人像。 */
|
|
585
|
+
function AuthorsIcon() {
|
|
586
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
587
|
+
...fieldSvgProps,
|
|
588
|
+
children: [
|
|
589
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
590
|
+
cx: "9",
|
|
591
|
+
cy: "8",
|
|
592
|
+
r: "3"
|
|
593
|
+
}),
|
|
594
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.5 20v-1a5.5 5.5 0 0 1 11 0v1" }),
|
|
595
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 5.4a3 3 0 0 1 0 5.2M18 15.2a5 5 0 0 1 2.5 3.8v1" })
|
|
596
|
+
]
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
/** 摘要:段落横排文字。 */
|
|
600
|
+
function AbstractIcon() {
|
|
601
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
602
|
+
...fieldSvgProps,
|
|
603
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 6h16M4 10h16M4 14h13M4 18h8" })
|
|
604
|
+
});
|
|
605
|
+
}
|
|
548
606
|
// src/client/locales.ts
|
|
549
607
|
/**
|
|
550
608
|
* 文案字典(zh / en)。
|
|
@@ -599,6 +657,9 @@ const dicts = {
|
|
|
599
657
|
"task.refresh": "刷新",
|
|
600
658
|
"task.rerun": "重跑",
|
|
601
659
|
"task.cancel": "取消",
|
|
660
|
+
"task.insertToChat": "带入对话",
|
|
661
|
+
"task.insertToChatHint": "把任务 id 与当前检索条件写入会话草稿;建议一个对话只跟进一个检索任务",
|
|
662
|
+
"task.inserted": "已把任务信息带入会话草稿",
|
|
602
663
|
"task.pin": "置顶",
|
|
603
664
|
"task.unpin": "取消置顶",
|
|
604
665
|
"task.running": "检索中…",
|
|
@@ -651,12 +712,19 @@ const dicts = {
|
|
|
651
712
|
"filter.authorPlaceholder": "完整姓名",
|
|
652
713
|
"filter.group": "分组",
|
|
653
714
|
"filter.any": "不限",
|
|
715
|
+
"filter.keywordChip": "关键词:{value}",
|
|
716
|
+
"filter.sourceChip": "来源:{value}",
|
|
717
|
+
"filter.yearChip": "年份:{value}",
|
|
718
|
+
"filter.screeningChip": "状态:{value}",
|
|
719
|
+
"filter.languageChip": "语言:{value}",
|
|
720
|
+
"filter.sortChip": "排序:{value}",
|
|
654
721
|
"filter.typeChip": "类型:{value}",
|
|
655
722
|
"filter.queryChip": "检索式:{value}",
|
|
656
723
|
"filter.groupChip": "分组:{value}",
|
|
657
724
|
"filter.venueChip": "出处:{value}",
|
|
658
725
|
"filter.authorChip": "作者:{value}",
|
|
659
726
|
"filter.newChip": "仅新增",
|
|
727
|
+
"filter.pdfStatusChip": "全文状态:{value}",
|
|
660
728
|
"filter.noMatch": "当前筛选没有匹配文献,试试放宽条件。",
|
|
661
729
|
"ledger.export": "导出",
|
|
662
730
|
"export.csv": "CSV 台账",
|
|
@@ -701,14 +769,21 @@ const dicts = {
|
|
|
701
769
|
"bulk.export": "按选中导出",
|
|
702
770
|
"bulk.downloadDone": "批量下载:成功 {ok} 篇,失败 {failed} 篇",
|
|
703
771
|
"fulltext.loading": "统计全文覆盖…",
|
|
704
|
-
"fulltext.hint": "
|
|
705
|
-
"fulltext.
|
|
772
|
+
"fulltext.hint": "统计基于全部 {total} 篇,与概览同口径,不受当前筛选影响;点状态可切换清单,列表分页加载。",
|
|
773
|
+
"fulltext.hintFallback": "统计暂不可用,仍可按状态查看文献清单。",
|
|
774
|
+
"fulltext.coverage": "可获取全文 {available}/{total} 篇(含已下载与 HTML 全文)",
|
|
706
775
|
"fulltext.missing": "缺全文",
|
|
707
|
-
"fulltext.showInResults": "
|
|
708
|
-
"fulltext.none": "
|
|
709
|
-
"fulltext.
|
|
776
|
+
"fulltext.showInResults": "在结果中查看全部 {n} 篇 →",
|
|
777
|
+
"fulltext.none": "当前视图没有符合条件的文献。",
|
|
778
|
+
"fulltext.loadMore": "加载更多(已显示 {shown}/{total})",
|
|
779
|
+
"fulltext.coverageBar": "全文覆盖 {available}/{total}({rate}%)· {missing} 篇待补",
|
|
780
|
+
"fulltext.barHint": "全任务口径,不受当前筛选影响",
|
|
781
|
+
"fulltext.openTab": "全文台账 →",
|
|
782
|
+
"fulltext.quickFilter": "只看缺全文",
|
|
710
783
|
"annotate.group": "分组",
|
|
711
784
|
"annotate.noGroup": "未分组",
|
|
785
|
+
"annotate.createGroup": "+ 新建分组…",
|
|
786
|
+
"annotate.groupHint": "还没有分组;可在任务的「分组」页新建,选「未分组」可清除分组",
|
|
712
787
|
"annotate.expand": "标注",
|
|
713
788
|
"annotate.collapse": "收起标注",
|
|
714
789
|
"annotate.note": "备注",
|
|
@@ -750,7 +825,6 @@ const dicts = {
|
|
|
750
825
|
"groups.name": "名称",
|
|
751
826
|
"groups.desc": "说明",
|
|
752
827
|
"groups.add": "添加分组",
|
|
753
|
-
"groups.save": "保存分组",
|
|
754
828
|
"groups.delete": "删除",
|
|
755
829
|
"groups.up": "上移",
|
|
756
830
|
"groups.down": "下移",
|
|
@@ -761,7 +835,8 @@ const dicts = {
|
|
|
761
835
|
"groups.descPlaceholder": "如:用于方法对比",
|
|
762
836
|
"groups.deleteConfirm": "删除分组「{name}」?组内文献会回到未分组,标注不受影响。",
|
|
763
837
|
"groups.saved": "分组已保存",
|
|
764
|
-
"groups.
|
|
838
|
+
"groups.saving": "正在保存…",
|
|
839
|
+
"groups.retry": "重试保存",
|
|
765
840
|
"notes.title": "任务笔记",
|
|
766
841
|
"notes.taskPlaceholder": "记录检索策略、纳入排除标准、阶段性结论…",
|
|
767
842
|
"notes.save": "保存笔记",
|
|
@@ -846,6 +921,10 @@ const dicts = {
|
|
|
846
921
|
"result.insert": "插入到对话",
|
|
847
922
|
"result.inserted": "已插入到会话草稿",
|
|
848
923
|
"result.pdf": "PDF",
|
|
924
|
+
"result.infoLabel": "状态与来源",
|
|
925
|
+
"result.infoHint": "筛读状态、命中的数据库、被引次数、开放获取与全文状态",
|
|
926
|
+
"result.actionLabel": "快捷操作",
|
|
927
|
+
"result.actionHint": "对这篇文献执行操作:插入对话、打开详情、预览或下载全文、存入 Zotero",
|
|
849
928
|
"error.noEditor": "当前面板不在会话上下文中,无法插入到输入框",
|
|
850
929
|
"error.draftChanged": "输入框草稿已变化,未插入;请避免在插入期间编辑草稿",
|
|
851
930
|
"error.sourceFailed": "数据源 {source} 检索失败:{message}",
|
|
@@ -973,6 +1052,14 @@ const dicts = {
|
|
|
973
1052
|
"cmp.paper.url": "链接:{value}",
|
|
974
1053
|
"cmp.paper.pdf": "PDF:{value}",
|
|
975
1054
|
"cmp.paper.abstract": "摘要:{value}",
|
|
1055
|
+
"cmp.task.header": "【文献检索任务】请继续使用下面这个现有任务,不要新建任务",
|
|
1056
|
+
"cmp.task.id": "任务 id:{value}",
|
|
1057
|
+
"cmp.task.title": "任务标题:{value}",
|
|
1058
|
+
"cmp.task.rounds": "已有 {n} 轮检索,已加载 {loaded} 篇",
|
|
1059
|
+
"cmp.task.queries": "当前检索式:",
|
|
1060
|
+
"cmp.task.conditions": "检索条件:数据源 {sources};每源 {pageSize} 条;年份 {years};{access};语言 {language};排序 {sort}",
|
|
1061
|
+
"cmp.task.anyAccess": "不限获取方式",
|
|
1062
|
+
"cmp.task.continue": "后续需要调整条件继续检索时,调用 literature_search 并传入 taskId「{id}」,在这个任务内开新一轮;只有我明确要求新建查询任务时才新建。",
|
|
976
1063
|
"rounds.open": "查看检索历史",
|
|
977
1064
|
"rounds.title": "检索轮次历史",
|
|
978
1065
|
"rounds.desc": "共 {n} 轮检索;只记录条件与命中,结果始终是最新一轮",
|
|
@@ -994,7 +1081,7 @@ const dicts = {
|
|
|
994
1081
|
"rounds.badge": "{n} 轮",
|
|
995
1082
|
"taskList.open": "搜索任务列表",
|
|
996
1083
|
"taskList.title": "搜索任务",
|
|
997
|
-
"taskList.desc": "
|
|
1084
|
+
"taskList.desc": "一个研究主题对应一个任务;每个对话只保留一个任务 tab,切换任务请在这里进行。",
|
|
998
1085
|
"taskList.count": "共 {n} 个任务",
|
|
999
1086
|
"taskList.close": "关闭",
|
|
1000
1087
|
"taskList.loading": "正在读取任务…",
|
|
@@ -1046,6 +1133,9 @@ const dicts = {
|
|
|
1046
1133
|
"task.refresh": "Refresh",
|
|
1047
1134
|
"task.rerun": "Run again",
|
|
1048
1135
|
"task.cancel": "Cancel",
|
|
1136
|
+
"task.insertToChat": "Add to chat",
|
|
1137
|
+
"task.insertToChatHint": "Insert the task id and current conditions into the chat draft; prefer one conversation per search task",
|
|
1138
|
+
"task.inserted": "Task details added to the chat draft",
|
|
1049
1139
|
"task.pin": "Pin",
|
|
1050
1140
|
"task.unpin": "Unpin",
|
|
1051
1141
|
"task.running": "Searching…",
|
|
@@ -1098,12 +1188,19 @@ const dicts = {
|
|
|
1098
1188
|
"filter.authorPlaceholder": "full name",
|
|
1099
1189
|
"filter.group": "Group",
|
|
1100
1190
|
"filter.any": "Any",
|
|
1191
|
+
"filter.keywordChip": "Keyword: {value}",
|
|
1192
|
+
"filter.sourceChip": "Source: {value}",
|
|
1193
|
+
"filter.yearChip": "Year: {value}",
|
|
1194
|
+
"filter.screeningChip": "Status: {value}",
|
|
1195
|
+
"filter.languageChip": "Language: {value}",
|
|
1196
|
+
"filter.sortChip": "Sort: {value}",
|
|
1101
1197
|
"filter.typeChip": "Type: {value}",
|
|
1102
1198
|
"filter.queryChip": "Query: {value}",
|
|
1103
1199
|
"filter.groupChip": "Group: {value}",
|
|
1104
1200
|
"filter.venueChip": "Venue: {value}",
|
|
1105
1201
|
"filter.authorChip": "Author: {value}",
|
|
1106
1202
|
"filter.newChip": "New only",
|
|
1203
|
+
"filter.pdfStatusChip": "Full-text status: {value}",
|
|
1107
1204
|
"filter.noMatch": "No papers match the current filters; try relaxing them.",
|
|
1108
1205
|
"ledger.export": "Export",
|
|
1109
1206
|
"export.csv": "CSV ledger",
|
|
@@ -1148,14 +1245,21 @@ const dicts = {
|
|
|
1148
1245
|
"bulk.export": "Export selected",
|
|
1149
1246
|
"bulk.downloadDone": "Download: {ok} succeeded, {failed} failed",
|
|
1150
1247
|
"fulltext.loading": "Computing full-text coverage…",
|
|
1151
|
-
"fulltext.hint": "Coverage
|
|
1152
|
-
"fulltext.
|
|
1248
|
+
"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.",
|
|
1249
|
+
"fulltext.hintFallback": "Coverage is unavailable; you can still browse papers by status.",
|
|
1250
|
+
"fulltext.coverage": "Full text reachable for {available}/{total} papers (including downloads and HTML)",
|
|
1153
1251
|
"fulltext.missing": "Missing full text",
|
|
1154
|
-
"fulltext.showInResults": "
|
|
1155
|
-
"fulltext.none": "No papers
|
|
1156
|
-
"fulltext.
|
|
1252
|
+
"fulltext.showInResults": "View all {n} in results →",
|
|
1253
|
+
"fulltext.none": "No papers match this view.",
|
|
1254
|
+
"fulltext.loadMore": "Load more (showing {shown}/{total})",
|
|
1255
|
+
"fulltext.coverageBar": "Full text {available}/{total} ({rate}%) · {missing} to fetch",
|
|
1256
|
+
"fulltext.barHint": "Task-wide basis, unaffected by current filters",
|
|
1257
|
+
"fulltext.openTab": "Full-text ledger →",
|
|
1258
|
+
"fulltext.quickFilter": "Missing full text only",
|
|
1157
1259
|
"annotate.group": "Group",
|
|
1158
1260
|
"annotate.noGroup": "Ungrouped",
|
|
1261
|
+
"annotate.createGroup": "+ New group…",
|
|
1262
|
+
"annotate.groupHint": "No groups yet; create one on the Groups tab, or pick Ungrouped to clear",
|
|
1159
1263
|
"annotate.expand": "Annotate",
|
|
1160
1264
|
"annotate.collapse": "Hide annotation",
|
|
1161
1265
|
"annotate.note": "Note",
|
|
@@ -1197,7 +1301,6 @@ const dicts = {
|
|
|
1197
1301
|
"groups.name": "Name",
|
|
1198
1302
|
"groups.desc": "Description",
|
|
1199
1303
|
"groups.add": "Add group",
|
|
1200
|
-
"groups.save": "Save groups",
|
|
1201
1304
|
"groups.delete": "Delete",
|
|
1202
1305
|
"groups.up": "Move up",
|
|
1203
1306
|
"groups.down": "Move down",
|
|
@@ -1208,7 +1311,8 @@ const dicts = {
|
|
|
1208
1311
|
"groups.descPlaceholder": "e.g. For method comparison",
|
|
1209
1312
|
"groups.deleteConfirm": "Delete group \"{name}\"? Its papers become ungrouped; annotations are kept.",
|
|
1210
1313
|
"groups.saved": "Groups saved",
|
|
1211
|
-
"groups.
|
|
1314
|
+
"groups.saving": "Saving…",
|
|
1315
|
+
"groups.retry": "Retry saving",
|
|
1212
1316
|
"notes.title": "Task notes",
|
|
1213
1317
|
"notes.taskPlaceholder": "Search strategy, inclusion criteria, interim conclusions…",
|
|
1214
1318
|
"notes.save": "Save notes",
|
|
@@ -1293,6 +1397,10 @@ const dicts = {
|
|
|
1293
1397
|
"result.insert": "Add to conversation",
|
|
1294
1398
|
"result.inserted": "Added to the conversation draft",
|
|
1295
1399
|
"result.pdf": "PDF",
|
|
1400
|
+
"result.infoLabel": "Status & sources",
|
|
1401
|
+
"result.infoHint": "Screening status, matched databases, citations, open access and full-text status",
|
|
1402
|
+
"result.actionLabel": "Actions",
|
|
1403
|
+
"result.actionHint": "Actions on this paper: insert into chat, open details, preview or download full text, save to Zotero",
|
|
1296
1404
|
"error.noEditor": "This panel is not in a conversation context; cannot insert into the composer",
|
|
1297
1405
|
"error.draftChanged": "The draft changed; nothing was inserted. Avoid editing while inserting.",
|
|
1298
1406
|
"error.sourceFailed": "Source {source} failed: {message}",
|
|
@@ -1420,6 +1528,14 @@ const dicts = {
|
|
|
1420
1528
|
"cmp.paper.url": "URL: {value}",
|
|
1421
1529
|
"cmp.paper.pdf": "PDF: {value}",
|
|
1422
1530
|
"cmp.paper.abstract": "Abstract: {value}",
|
|
1531
|
+
"cmp.task.header": "[Literature task] Continue using the existing task below; do not create a new task",
|
|
1532
|
+
"cmp.task.id": "Task id: {value}",
|
|
1533
|
+
"cmp.task.title": "Task title: {value}",
|
|
1534
|
+
"cmp.task.rounds": "{n} rounds so far, {loaded} papers loaded",
|
|
1535
|
+
"cmp.task.queries": "Current queries:",
|
|
1536
|
+
"cmp.task.conditions": "Conditions: sources {sources}; {pageSize} per source; years {years}; {access}; language {language}; sort {sort}",
|
|
1537
|
+
"cmp.task.anyAccess": "any access",
|
|
1538
|
+
"cmp.task.continue": "To adjust conditions and search again, call literature_search with taskId \"{id}\" to start a new round inside this task; create a new task only when I explicitly ask for a new query task.",
|
|
1423
1539
|
"rounds.open": "Search rounds",
|
|
1424
1540
|
"rounds.title": "Search rounds",
|
|
1425
1541
|
"rounds.desc": "{n} rounds; metadata only, results are always from the latest round",
|
|
@@ -1441,7 +1557,7 @@ const dicts = {
|
|
|
1441
1557
|
"rounds.badge": "{n} rounds",
|
|
1442
1558
|
"taskList.open": "Search tasks",
|
|
1443
1559
|
"taskList.title": "Search tasks",
|
|
1444
|
-
"taskList.desc": "One research topic = one task
|
|
1560
|
+
"taskList.desc": "One research topic = one task. Each conversation keeps a single task tab — switch tasks here.",
|
|
1445
1561
|
"taskList.count": "{n} tasks",
|
|
1446
1562
|
"taskList.close": "Close",
|
|
1447
1563
|
"taskList.loading": "Loading tasks…",
|
|
@@ -1464,12 +1580,12 @@ const dicts = {
|
|
|
1464
1580
|
const ACCENT = "#508cff";
|
|
1465
1581
|
/** 错误色。 */
|
|
1466
1582
|
const ERROR = "#d64545";
|
|
1467
|
-
/**
|
|
1583
|
+
/** 面板根容器(主页 / 任务页 / 论文页共用):上下 10px、左右 15px 页面边距。 */
|
|
1468
1584
|
const panelStyle = {
|
|
1469
1585
|
display: "flex",
|
|
1470
1586
|
flexDirection: "column",
|
|
1471
1587
|
gap: 10,
|
|
1472
|
-
padding: "
|
|
1588
|
+
padding: "10px 15px",
|
|
1473
1589
|
minHeight: 0,
|
|
1474
1590
|
fontSize: 13
|
|
1475
1591
|
};
|
|
@@ -1507,10 +1623,10 @@ const listStyle$1 = {
|
|
|
1507
1623
|
flexDirection: "column",
|
|
1508
1624
|
gap: 0
|
|
1509
1625
|
};
|
|
1510
|
-
/**
|
|
1626
|
+
/** 单条结果卡片:顶线即文献之间的分割线,透明度需足够高才看得清。 */
|
|
1511
1627
|
const cardStyle = {
|
|
1512
1628
|
padding: "10px 0",
|
|
1513
|
-
borderTop: "1px solid rgba(127,127,127,0.
|
|
1629
|
+
borderTop: "1px solid rgba(127,127,127,0.5)"
|
|
1514
1630
|
};
|
|
1515
1631
|
/** 标题。 */
|
|
1516
1632
|
const titleStyle = {
|
|
@@ -1623,7 +1739,7 @@ function formatDate$3(value) {
|
|
|
1623
1739
|
const date = new Date(value);
|
|
1624
1740
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
1625
1741
|
}
|
|
1626
|
-
function TaskListDialog({ t, open, onClose, currentTaskId, onOpenTask }) {
|
|
1742
|
+
function TaskListDialog({ t, open, onClose, currentTaskId, onOpenTask, onCurrentRemoved }) {
|
|
1627
1743
|
const version = useTaskListVersion();
|
|
1628
1744
|
const [tasks, setTasks] = (0, react.useState)([]);
|
|
1629
1745
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
@@ -1690,11 +1806,17 @@ function TaskListDialog({ t, open, onClose, currentTaskId, onOpenTask }) {
|
|
|
1690
1806
|
const remove = async (task) => {
|
|
1691
1807
|
if (busyId !== null) return;
|
|
1692
1808
|
setBusyId(task.id);
|
|
1809
|
+
const index = tasks.findIndex((entry) => entry.id === task.id);
|
|
1693
1810
|
try {
|
|
1694
1811
|
await deleteTask(task.id);
|
|
1695
1812
|
forgetTask(task.id);
|
|
1696
1813
|
notifyTaskListChanged();
|
|
1697
1814
|
setConfirmDeleteId(null);
|
|
1815
|
+
if (task.id === currentTaskId) {
|
|
1816
|
+
const remaining = tasks.filter((entry) => entry.id !== task.id);
|
|
1817
|
+
const fallback = index < 0 ? null : remaining[index - 1] ?? remaining[index] ?? null;
|
|
1818
|
+
onCurrentRemoved?.(fallback);
|
|
1819
|
+
}
|
|
1698
1820
|
await load();
|
|
1699
1821
|
} catch (err) {
|
|
1700
1822
|
setError(messageOf$4(err));
|
|
@@ -2474,6 +2596,36 @@ function paperText(t, paper) {
|
|
|
2474
2596
|
function papersText(t, papers) {
|
|
2475
2597
|
return papers.map((paper, index) => `${index + 1}. ${paperText(t, paper)}`).join("\n\n");
|
|
2476
2598
|
}
|
|
2599
|
+
/**
|
|
2600
|
+
* 把任务带到会话:写入任务 id 与当前条件,让新会话能明确续接同一任务。
|
|
2601
|
+
*
|
|
2602
|
+
* 模型侧只要收到带 taskId 的指令就不会新建任务,因此这里把「不要新建」和
|
|
2603
|
+
* 续接方式都写清楚。
|
|
2604
|
+
*/
|
|
2605
|
+
function taskText(t, detail) {
|
|
2606
|
+
const conditions = detail.conditions;
|
|
2607
|
+
const queries = conditions.queries.filter((query) => query.enabled && query.text.trim() !== "").map((query) => `- ${query.name}: ${query.text}`);
|
|
2608
|
+
const lines = [
|
|
2609
|
+
t("cmp.task.header"),
|
|
2610
|
+
tpl(t("cmp.task.id"), { value: detail.id }),
|
|
2611
|
+
tpl(t("cmp.task.title"), { value: detail.title }),
|
|
2612
|
+
tpl(t("cmp.task.rounds"), {
|
|
2613
|
+
n: detail.roundCount ?? 0,
|
|
2614
|
+
loaded: detail.result.totalLoaded
|
|
2615
|
+
})
|
|
2616
|
+
];
|
|
2617
|
+
if (queries.length > 0) lines.push(t("cmp.task.queries"), ...queries);
|
|
2618
|
+
lines.push(tpl(t("cmp.task.conditions"), {
|
|
2619
|
+
sources: conditions.sources.map((id) => t(`source.${id}`)).join("、"),
|
|
2620
|
+
pageSize: conditions.pageSize,
|
|
2621
|
+
years: `${conditions.yearFrom ?? t("filter.any")}-${conditions.yearTo ?? t("filter.any")}`,
|
|
2622
|
+
access: conditions.openAccessOnly ? t("filter.openAccess") : t("cmp.task.anyAccess"),
|
|
2623
|
+
language: t(`lang.${conditions.language}`),
|
|
2624
|
+
sort: t(`sort.${conditions.sort}`)
|
|
2625
|
+
}));
|
|
2626
|
+
lines.push("", tpl(t("cmp.task.continue"), { id: detail.id }));
|
|
2627
|
+
return lines.join("\n");
|
|
2628
|
+
}
|
|
2477
2629
|
function truncate(value, max) {
|
|
2478
2630
|
return value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
2479
2631
|
}
|
|
@@ -2566,7 +2718,7 @@ function EntityPanel({ t, taskId, paperId, pmid }) {
|
|
|
2566
2718
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2567
2719
|
style: CHIPS_STYLE,
|
|
2568
2720
|
children: items.map((entity) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2569
|
-
style: CHIP_STYLE$
|
|
2721
|
+
style: CHIP_STYLE$2,
|
|
2570
2722
|
title: entityTitle(entity),
|
|
2571
2723
|
children: [entity.text, entity.count > 1 ? ` ×${entity.count}` : ""]
|
|
2572
2724
|
}, `${entity.identifier}\u0000${entity.text}`))
|
|
@@ -2627,7 +2779,7 @@ const CHIPS_STYLE = {
|
|
|
2627
2779
|
gap: 4,
|
|
2628
2780
|
minWidth: 0
|
|
2629
2781
|
};
|
|
2630
|
-
const CHIP_STYLE$
|
|
2782
|
+
const CHIP_STYLE$2 = {
|
|
2631
2783
|
padding: "0 6px",
|
|
2632
2784
|
borderRadius: 9,
|
|
2633
2785
|
border: "1px solid rgba(127,127,127,0.28)",
|
|
@@ -2935,8 +3087,9 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
2935
3087
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2936
3088
|
style: SELECT_STYLE$3,
|
|
2937
3089
|
value: state?.groupId ?? "",
|
|
2938
|
-
disabled: busy
|
|
3090
|
+
disabled: busy,
|
|
2939
3091
|
"aria-label": t("annotate.group"),
|
|
3092
|
+
title: detail.groups.length === 0 ? t("annotate.groupHint") : t("annotate.group"),
|
|
2940
3093
|
onChange: (event) => annotate({
|
|
2941
3094
|
paperId: paper.paperId,
|
|
2942
3095
|
groupId: event.target.value === "" ? null : event.target.value
|
|
@@ -2961,7 +3114,7 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
2961
3114
|
style: EDIT_STYLE$1,
|
|
2962
3115
|
children: [
|
|
2963
3116
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2964
|
-
style: FIELD_STYLE$
|
|
3117
|
+
style: FIELD_STYLE$4,
|
|
2965
3118
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.note") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
2966
3119
|
style: NOTE_STYLE$1,
|
|
2967
3120
|
value: note,
|
|
@@ -2971,7 +3124,7 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
2971
3124
|
})]
|
|
2972
3125
|
}),
|
|
2973
3126
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2974
|
-
style: FIELD_STYLE$
|
|
3127
|
+
style: FIELD_STYLE$4,
|
|
2975
3128
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.tags") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2976
3129
|
style: INPUT_STYLE$3,
|
|
2977
3130
|
value: tags,
|
|
@@ -2981,7 +3134,7 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
2981
3134
|
}),
|
|
2982
3135
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2983
3136
|
style: {
|
|
2984
|
-
...FIELD_STYLE$
|
|
3137
|
+
...FIELD_STYLE$4,
|
|
2985
3138
|
maxWidth: 120
|
|
2986
3139
|
},
|
|
2987
3140
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.shortlistOrder") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -3192,7 +3345,7 @@ const EDIT_STYLE$1 = {
|
|
|
3192
3345
|
borderRadius: 6,
|
|
3193
3346
|
background: "rgba(127,127,127,0.07)"
|
|
3194
3347
|
};
|
|
3195
|
-
const FIELD_STYLE$
|
|
3348
|
+
const FIELD_STYLE$4 = {
|
|
3196
3349
|
display: "flex",
|
|
3197
3350
|
flexDirection: "column",
|
|
3198
3351
|
gap: 2,
|
|
@@ -4374,7 +4527,7 @@ function CompareTab({ t, taskId, fromTaskId, rev, busy = false, onOnlyNew, onInc
|
|
|
4374
4527
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4375
4528
|
style: PICK_ROW_STYLE,
|
|
4376
4529
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4377
|
-
style: FIELD_STYLE$
|
|
4530
|
+
style: FIELD_STYLE$3,
|
|
4378
4531
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("compare.base") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4379
4532
|
style: SELECT_STYLE$2,
|
|
4380
4533
|
value: baseId,
|
|
@@ -4492,7 +4645,7 @@ const PICK_ROW_STYLE = {
|
|
|
4492
4645
|
gap: 8,
|
|
4493
4646
|
flexWrap: "wrap"
|
|
4494
4647
|
};
|
|
4495
|
-
const FIELD_STYLE$
|
|
4648
|
+
const FIELD_STYLE$3 = {
|
|
4496
4649
|
display: "flex",
|
|
4497
4650
|
flexDirection: "column",
|
|
4498
4651
|
gap: 2,
|
|
@@ -4693,7 +4846,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4693
4846
|
style: ROW_STYLE$1,
|
|
4694
4847
|
children: [
|
|
4695
4848
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4696
|
-
style: FIELD_STYLE$
|
|
4849
|
+
style: FIELD_STYLE$2,
|
|
4697
4850
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4698
4851
|
style: FIELD_LABEL_STYLE,
|
|
4699
4852
|
children: t("condition.pageSize")
|
|
@@ -4707,7 +4860,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4707
4860
|
})]
|
|
4708
4861
|
}),
|
|
4709
4862
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4710
|
-
style: FIELD_STYLE$
|
|
4863
|
+
style: FIELD_STYLE$2,
|
|
4711
4864
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4712
4865
|
style: FIELD_LABEL_STYLE,
|
|
4713
4866
|
children: t("condition.yearFrom")
|
|
@@ -4720,7 +4873,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4720
4873
|
})]
|
|
4721
4874
|
}),
|
|
4722
4875
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4723
|
-
style: FIELD_STYLE$
|
|
4876
|
+
style: FIELD_STYLE$2,
|
|
4724
4877
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4725
4878
|
style: FIELD_LABEL_STYLE,
|
|
4726
4879
|
children: t("condition.yearTo")
|
|
@@ -4738,7 +4891,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4738
4891
|
style: ROW_STYLE$1,
|
|
4739
4892
|
children: [
|
|
4740
4893
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4741
|
-
style: CHECK_STYLE$
|
|
4894
|
+
style: CHECK_STYLE$2,
|
|
4742
4895
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4743
4896
|
type: "checkbox",
|
|
4744
4897
|
checked: openAccessOnly,
|
|
@@ -4746,7 +4899,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4746
4899
|
}), t("condition.openAccess")]
|
|
4747
4900
|
}),
|
|
4748
4901
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4749
|
-
style: FIELD_STYLE$
|
|
4902
|
+
style: FIELD_STYLE$2,
|
|
4750
4903
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4751
4904
|
style: FIELD_LABEL_STYLE,
|
|
4752
4905
|
children: t("filter.language")
|
|
@@ -4761,7 +4914,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4761
4914
|
})]
|
|
4762
4915
|
}),
|
|
4763
4916
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4764
|
-
style: FIELD_STYLE$
|
|
4917
|
+
style: FIELD_STYLE$2,
|
|
4765
4918
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4766
4919
|
style: FIELD_LABEL_STYLE,
|
|
4767
4920
|
children: t("filter.sort")
|
|
@@ -4779,7 +4932,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
4779
4932
|
}),
|
|
4780
4933
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4781
4934
|
style: {
|
|
4782
|
-
...FIELD_STYLE$
|
|
4935
|
+
...FIELD_STYLE$2,
|
|
4783
4936
|
flexDirection: "column",
|
|
4784
4937
|
alignItems: "stretch"
|
|
4785
4938
|
},
|
|
@@ -4842,7 +4995,7 @@ const ROW_STYLE$1 = {
|
|
|
4842
4995
|
flexWrap: "wrap",
|
|
4843
4996
|
gap: 8
|
|
4844
4997
|
};
|
|
4845
|
-
const FIELD_STYLE$
|
|
4998
|
+
const FIELD_STYLE$2 = {
|
|
4846
4999
|
display: "flex",
|
|
4847
5000
|
alignItems: "center",
|
|
4848
5001
|
gap: 4
|
|
@@ -4864,7 +5017,7 @@ const SELECT_STYLE$1 = {
|
|
|
4864
5017
|
background: "transparent",
|
|
4865
5018
|
color: "inherit"
|
|
4866
5019
|
};
|
|
4867
|
-
const CHECK_STYLE$
|
|
5020
|
+
const CHECK_STYLE$2 = {
|
|
4868
5021
|
display: "flex",
|
|
4869
5022
|
alignItems: "center",
|
|
4870
5023
|
gap: 4,
|
|
@@ -4872,10 +5025,11 @@ const CHECK_STYLE$1 = {
|
|
|
4872
5025
|
};
|
|
4873
5026
|
// src/client/FullTextTab.tsx
|
|
4874
5027
|
/**
|
|
4875
|
-
*
|
|
5028
|
+
* 「全文」台账:全任务口径的覆盖率 + 可按状态切换的补抓队列。
|
|
4876
5029
|
*
|
|
4877
|
-
*
|
|
4878
|
-
*
|
|
5030
|
+
* 统计由父组件一次取回(与结果页的覆盖率横幅共用同一份数据),本页只按视图
|
|
5031
|
+
* 拉列表;切换视图或任务 rev 变化时重置分页。下载 / Zotero 走父组件的同一套
|
|
5032
|
+
* 操作,完成后 rev 变化会触发本页重取。
|
|
4879
5033
|
*/
|
|
4880
5034
|
const STATUSES = [
|
|
4881
5035
|
"unknown",
|
|
@@ -4886,159 +5040,284 @@ const STATUSES = [
|
|
|
4886
5040
|
"blocked",
|
|
4887
5041
|
"failed"
|
|
4888
5042
|
];
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
5043
|
+
const PAGE_SIZE = 100;
|
|
5044
|
+
const EMPTY_LIST = {
|
|
5045
|
+
papers: [],
|
|
5046
|
+
total: 0,
|
|
5047
|
+
loading: true,
|
|
5048
|
+
loadingMore: false,
|
|
5049
|
+
error: null
|
|
5050
|
+
};
|
|
5051
|
+
function viewQuery(view) {
|
|
5052
|
+
return view === "missing" ? { missingFullText: true } : { pdfStatus: view };
|
|
5053
|
+
}
|
|
5054
|
+
function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBatchDownload, onBatchZotero, onShowInResults }) {
|
|
5055
|
+
const [view, setView] = (0, react.useState)("missing");
|
|
5056
|
+
const [list, setList] = (0, react.useState)(EMPTY_LIST);
|
|
5057
|
+
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
5058
|
+
const seqRef = (0, react.useRef)(0);
|
|
4896
5059
|
(0, react.useEffect)(() => {
|
|
5060
|
+
const seq = ++seqRef.current;
|
|
4897
5061
|
const controller = new AbortController();
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
stats,
|
|
4909
|
-
page,
|
|
5062
|
+
setList(EMPTY_LIST);
|
|
5063
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
5064
|
+
fetchResults(taskId, {
|
|
5065
|
+
...viewQuery(view),
|
|
5066
|
+
limit: PAGE_SIZE
|
|
5067
|
+
}, controller.signal).then((page) => {
|
|
5068
|
+
if (seqRef.current !== seq || controller.signal.aborted) return;
|
|
5069
|
+
setList({
|
|
5070
|
+
papers: page.papers,
|
|
5071
|
+
total: page.total,
|
|
4910
5072
|
loading: false,
|
|
5073
|
+
loadingMore: false,
|
|
4911
5074
|
error: null
|
|
4912
5075
|
});
|
|
4913
5076
|
}).catch((error) => {
|
|
4914
|
-
if (controller.signal.aborted) return;
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
page: null,
|
|
5077
|
+
if (seqRef.current !== seq || controller.signal.aborted) return;
|
|
5078
|
+
setList({
|
|
5079
|
+
...EMPTY_LIST,
|
|
4918
5080
|
loading: false,
|
|
4919
5081
|
error: error instanceof Error ? error.message : String(error)
|
|
4920
5082
|
});
|
|
4921
5083
|
});
|
|
4922
|
-
return () =>
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
5084
|
+
return () => controller.abort();
|
|
5085
|
+
}, [
|
|
5086
|
+
taskId,
|
|
5087
|
+
rev,
|
|
5088
|
+
view
|
|
5089
|
+
]);
|
|
5090
|
+
const loadMore = () => {
|
|
5091
|
+
if (list.loading || list.loadingMore || list.papers.length >= list.total) return;
|
|
5092
|
+
const seq = seqRef.current;
|
|
5093
|
+
const offset = list.papers.length;
|
|
5094
|
+
setList((previous) => ({
|
|
5095
|
+
...previous,
|
|
5096
|
+
loadingMore: true
|
|
5097
|
+
}));
|
|
5098
|
+
fetchResults(taskId, {
|
|
5099
|
+
...viewQuery(view),
|
|
5100
|
+
offset,
|
|
5101
|
+
limit: PAGE_SIZE
|
|
5102
|
+
}).then((page) => {
|
|
5103
|
+
if (seqRef.current !== seq) return;
|
|
5104
|
+
setList((previous) => ({
|
|
5105
|
+
...previous,
|
|
5106
|
+
papers: [...previous.papers, ...page.papers],
|
|
5107
|
+
total: page.total,
|
|
5108
|
+
loadingMore: false
|
|
5109
|
+
}));
|
|
5110
|
+
}).catch((error) => {
|
|
5111
|
+
if (seqRef.current !== seq) return;
|
|
5112
|
+
setList((previous) => ({
|
|
5113
|
+
...previous,
|
|
5114
|
+
loadingMore: false,
|
|
5115
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5116
|
+
}));
|
|
5117
|
+
});
|
|
5118
|
+
};
|
|
5119
|
+
const toggleSelect = (paperId) => {
|
|
5120
|
+
setSelected((previous) => {
|
|
5121
|
+
const next = new Set(previous);
|
|
5122
|
+
if (next.has(paperId)) next.delete(paperId);
|
|
5123
|
+
else next.add(paperId);
|
|
5124
|
+
return next;
|
|
5125
|
+
});
|
|
5126
|
+
};
|
|
5127
|
+
if (list.loading && list.papers.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4927
5128
|
style: TEXT_STYLE$1,
|
|
4928
5129
|
children: t("fulltext.loading")
|
|
4929
5130
|
});
|
|
4930
|
-
if (state.error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4931
|
-
style: {
|
|
4932
|
-
...TEXT_STYLE$1,
|
|
4933
|
-
color: "#d64545"
|
|
4934
|
-
},
|
|
4935
|
-
children: state.error
|
|
4936
|
-
});
|
|
4937
|
-
const stats = state.stats;
|
|
4938
|
-
const page = state.page;
|
|
4939
5131
|
const coverage = stats?.fullText;
|
|
5132
|
+
const availableCount = coverage === void 0 ? 0 : coverage.available + coverage.downloaded + coverage.htmlOnly;
|
|
5133
|
+
const missingCount = coverage === void 0 ? 0 : coverage.unknown + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
5134
|
+
const selectedPapers = list.papers.filter((paper) => selected.has(paper.paperId));
|
|
5135
|
+
const viewLabel = view === "missing" ? t("fulltext.missing") : t(`pdf.status.${view}`);
|
|
4940
5136
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4941
5137
|
style: WRAP_STYLE$4,
|
|
4942
5138
|
children: [
|
|
4943
5139
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4944
5140
|
style: HINT_STYLE,
|
|
4945
|
-
children: t("fulltext.hint")
|
|
5141
|
+
children: coverage === void 0 ? t("fulltext.hintFallback") : tpl(t("fulltext.hint"), { total: stats?.loaded ?? 0 })
|
|
4946
5142
|
}),
|
|
4947
|
-
coverage !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5143
|
+
coverage !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4948
5144
|
style: CHIP_ROW_STYLE$1,
|
|
4949
|
-
children:
|
|
4950
|
-
|
|
5145
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5146
|
+
type: "button",
|
|
5147
|
+
style: view === "missing" ? CHIP_ACTIVE_STYLE : CHIP_BUTTON_STYLE$1,
|
|
5148
|
+
onClick: () => setView("missing"),
|
|
5149
|
+
children: [
|
|
5150
|
+
t("fulltext.missing"),
|
|
5151
|
+
" ",
|
|
5152
|
+
missingCount
|
|
5153
|
+
]
|
|
5154
|
+
}), STATUSES.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5155
|
+
type: "button",
|
|
5156
|
+
style: view === status ? CHIP_ACTIVE_STYLE : CHIP_BUTTON_STYLE$1,
|
|
5157
|
+
onClick: () => setView(status),
|
|
4951
5158
|
children: [
|
|
4952
5159
|
t(`pdf.status.${status}`),
|
|
4953
5160
|
" ",
|
|
4954
5161
|
coverage[statusKey(status)]
|
|
4955
5162
|
]
|
|
4956
|
-
}, status))
|
|
4957
|
-
}),
|
|
4958
|
-
coverage !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5163
|
+
}, status))]
|
|
5164
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4959
5165
|
style: TEXT_STYLE$1,
|
|
4960
5166
|
children: tpl(t("fulltext.coverage"), {
|
|
4961
|
-
available:
|
|
5167
|
+
available: availableCount,
|
|
4962
5168
|
total: stats?.loaded ?? 0
|
|
4963
5169
|
})
|
|
4964
|
-
}),
|
|
5170
|
+
})] }),
|
|
4965
5171
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4966
5172
|
style: ROW_HEAD_STYLE,
|
|
4967
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5173
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
5174
|
+
viewLabel,
|
|
5175
|
+
"(",
|
|
5176
|
+
list.total,
|
|
5177
|
+
")"
|
|
5178
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4968
5179
|
type: "button",
|
|
4969
5180
|
style: LINK_STYLE,
|
|
4970
|
-
onClick:
|
|
4971
|
-
children: t("fulltext.showInResults")
|
|
5181
|
+
onClick: () => onShowInResults(view),
|
|
5182
|
+
children: tpl(t("fulltext.showInResults"), { n: list.total })
|
|
4972
5183
|
})]
|
|
4973
5184
|
}),
|
|
4974
|
-
|
|
5185
|
+
selected.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkBar$1, {
|
|
5186
|
+
t,
|
|
5187
|
+
count: selected.size,
|
|
5188
|
+
onSelectPage: () => setSelected(new Set(list.papers.map((paper) => paper.paperId))),
|
|
5189
|
+
onClear: () => setSelected(/* @__PURE__ */ new Set()),
|
|
5190
|
+
onDownload: () => onBatchDownload(selectedPapers),
|
|
5191
|
+
onZotero: () => onBatchZotero(selectedPapers)
|
|
5192
|
+
}),
|
|
5193
|
+
list.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5194
|
+
style: {
|
|
5195
|
+
...TEXT_STYLE$1,
|
|
5196
|
+
color: "#d64545"
|
|
5197
|
+
},
|
|
5198
|
+
children: list.error
|
|
5199
|
+
}),
|
|
5200
|
+
list.papers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4975
5201
|
style: TEXT_STYLE$1,
|
|
4976
5202
|
children: t("fulltext.none")
|
|
4977
5203
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4978
5204
|
style: LIST_STYLE$2,
|
|
4979
|
-
children:
|
|
5205
|
+
children: list.papers.map((paper) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FullTextRow, {
|
|
4980
5206
|
t,
|
|
4981
5207
|
paper,
|
|
5208
|
+
selected: selected.has(paper.paperId),
|
|
4982
5209
|
status: paperStatus(states, paper),
|
|
4983
5210
|
error: states[paper.paperId]?.pdfError,
|
|
5211
|
+
onToggleSelect: toggleSelect,
|
|
4984
5212
|
onDownload,
|
|
4985
5213
|
onZotero
|
|
4986
5214
|
}, paper.paperId))
|
|
4987
5215
|
}),
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
5216
|
+
list.papers.length < list.total && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5217
|
+
type: "button",
|
|
5218
|
+
style: MORE_STYLE,
|
|
5219
|
+
disabled: list.loadingMore,
|
|
5220
|
+
onClick: loadMore,
|
|
5221
|
+
children: tpl(t("fulltext.loadMore"), {
|
|
5222
|
+
shown: list.papers.length,
|
|
5223
|
+
total: list.total
|
|
4993
5224
|
})
|
|
4994
5225
|
})
|
|
4995
5226
|
]
|
|
4996
5227
|
});
|
|
4997
5228
|
}
|
|
4998
|
-
function
|
|
4999
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
5000
|
-
style:
|
|
5001
|
-
children: [
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
5008
|
-
style: ROW_TITLE_STYLE,
|
|
5009
|
-
href: paper.url,
|
|
5010
|
-
target: "_blank",
|
|
5011
|
-
rel: "noreferrer noopener",
|
|
5012
|
-
children: paper.title
|
|
5013
|
-
}),
|
|
5014
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5015
|
-
style: ROW_META_STYLE,
|
|
5016
|
-
children: [
|
|
5017
|
-
[paper.year === null ? "" : String(paper.year), paper.venue].filter((part) => part !== "").join(" · "),
|
|
5018
|
-
" ",
|
|
5019
|
-
"· ",
|
|
5020
|
-
t(`pdf.status.${status}`)
|
|
5021
|
-
]
|
|
5022
|
-
}),
|
|
5023
|
-
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5024
|
-
style: ROW_ERROR_STYLE,
|
|
5025
|
-
children: error
|
|
5026
|
-
})
|
|
5027
|
-
]
|
|
5028
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5029
|
-
style: ROW_ACTIONS_STYLE,
|
|
5030
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5229
|
+
function BulkBar$1({ t, count, onSelectPage, onClear, onDownload, onZotero }) {
|
|
5230
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5231
|
+
style: BULK_STYLE$1,
|
|
5232
|
+
children: [
|
|
5233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
5234
|
+
style: { fontSize: 12 },
|
|
5235
|
+
children: tpl(t("bulk.count"), { n: count })
|
|
5236
|
+
}),
|
|
5237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5031
5238
|
type: "button",
|
|
5032
5239
|
style: LINK_STYLE,
|
|
5033
|
-
onClick:
|
|
5034
|
-
children: t("
|
|
5035
|
-
}),
|
|
5240
|
+
onClick: onSelectPage,
|
|
5241
|
+
children: t("bulk.selectPage")
|
|
5242
|
+
}),
|
|
5243
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5036
5244
|
type: "button",
|
|
5037
5245
|
style: LINK_STYLE,
|
|
5038
|
-
onClick:
|
|
5039
|
-
children: t("
|
|
5040
|
-
})
|
|
5041
|
-
|
|
5246
|
+
onClick: onClear,
|
|
5247
|
+
children: t("bulk.clear")
|
|
5248
|
+
}),
|
|
5249
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5250
|
+
type: "button",
|
|
5251
|
+
style: LINK_STYLE,
|
|
5252
|
+
onClick: onDownload,
|
|
5253
|
+
children: t("bulk.download")
|
|
5254
|
+
}),
|
|
5255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5256
|
+
type: "button",
|
|
5257
|
+
style: LINK_STYLE,
|
|
5258
|
+
onClick: onZotero,
|
|
5259
|
+
children: t("bulk.zotero")
|
|
5260
|
+
})
|
|
5261
|
+
]
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
function FullTextRow({ t, paper, selected, status, error, onToggleSelect, onDownload, onZotero }) {
|
|
5265
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
5266
|
+
style: ROW_STYLE,
|
|
5267
|
+
children: [
|
|
5268
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5269
|
+
type: "checkbox",
|
|
5270
|
+
style: CHECK_STYLE$1,
|
|
5271
|
+
checked: selected,
|
|
5272
|
+
"aria-label": t("bulk.select"),
|
|
5273
|
+
onChange: () => onToggleSelect(paper.paperId)
|
|
5274
|
+
}),
|
|
5275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5276
|
+
style: {
|
|
5277
|
+
...ROW_MAIN_STYLE,
|
|
5278
|
+
flex: 1
|
|
5279
|
+
},
|
|
5280
|
+
children: [
|
|
5281
|
+
paper.url === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5282
|
+
style: ROW_TITLE_STYLE,
|
|
5283
|
+
children: paper.title
|
|
5284
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
5285
|
+
style: ROW_TITLE_STYLE,
|
|
5286
|
+
href: paper.url,
|
|
5287
|
+
target: "_blank",
|
|
5288
|
+
rel: "noreferrer noopener",
|
|
5289
|
+
children: paper.title
|
|
5290
|
+
}),
|
|
5291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5292
|
+
style: ROW_META_STYLE,
|
|
5293
|
+
children: [
|
|
5294
|
+
[paper.year === null ? "" : String(paper.year), paper.venue].filter((part) => part !== "").join(" · "),
|
|
5295
|
+
" ",
|
|
5296
|
+
"· ",
|
|
5297
|
+
t(`pdf.status.${status}`)
|
|
5298
|
+
]
|
|
5299
|
+
}),
|
|
5300
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5301
|
+
style: ROW_ERROR_STYLE,
|
|
5302
|
+
children: error
|
|
5303
|
+
})
|
|
5304
|
+
]
|
|
5305
|
+
}),
|
|
5306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5307
|
+
style: ROW_ACTIONS_STYLE,
|
|
5308
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5309
|
+
type: "button",
|
|
5310
|
+
style: LINK_STYLE,
|
|
5311
|
+
onClick: () => onDownload(paper),
|
|
5312
|
+
children: t("pdf.download")
|
|
5313
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5314
|
+
type: "button",
|
|
5315
|
+
style: LINK_STYLE,
|
|
5316
|
+
onClick: () => onZotero(paper),
|
|
5317
|
+
children: t("pdf.zotero")
|
|
5318
|
+
})]
|
|
5319
|
+
})
|
|
5320
|
+
]
|
|
5042
5321
|
});
|
|
5043
5322
|
}
|
|
5044
5323
|
function paperStatus(states, paper) {
|
|
@@ -5067,11 +5346,20 @@ const CHIP_ROW_STYLE$1 = {
|
|
|
5067
5346
|
flexWrap: "wrap",
|
|
5068
5347
|
gap: 4
|
|
5069
5348
|
};
|
|
5070
|
-
const
|
|
5349
|
+
const CHIP_BUTTON_STYLE$1 = {
|
|
5071
5350
|
fontSize: 11,
|
|
5072
5351
|
padding: "1px 6px",
|
|
5073
5352
|
borderRadius: 4,
|
|
5074
|
-
border: "1px solid rgba(127,127,127,0.28)"
|
|
5353
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
5354
|
+
background: "transparent",
|
|
5355
|
+
color: "inherit",
|
|
5356
|
+
cursor: "pointer"
|
|
5357
|
+
};
|
|
5358
|
+
const CHIP_ACTIVE_STYLE = {
|
|
5359
|
+
...CHIP_BUTTON_STYLE$1,
|
|
5360
|
+
borderColor: "#508cff",
|
|
5361
|
+
color: "#508cff",
|
|
5362
|
+
fontWeight: 600
|
|
5075
5363
|
};
|
|
5076
5364
|
const ROW_HEAD_STYLE = {
|
|
5077
5365
|
display: "flex",
|
|
@@ -5091,16 +5379,40 @@ const LINK_STYLE = {
|
|
|
5091
5379
|
cursor: "pointer",
|
|
5092
5380
|
whiteSpace: "nowrap"
|
|
5093
5381
|
};
|
|
5382
|
+
const BULK_STYLE$1 = {
|
|
5383
|
+
display: "flex",
|
|
5384
|
+
alignItems: "center",
|
|
5385
|
+
flexWrap: "wrap",
|
|
5386
|
+
gap: 8,
|
|
5387
|
+
padding: "4px 6px",
|
|
5388
|
+
borderRadius: 6,
|
|
5389
|
+
background: "rgba(80,140,255,0.08)",
|
|
5390
|
+
border: "1px solid rgba(80,140,255,0.25)"
|
|
5391
|
+
};
|
|
5392
|
+
const MORE_STYLE = {
|
|
5393
|
+
alignSelf: "flex-start",
|
|
5394
|
+
marginTop: 4,
|
|
5395
|
+
padding: "3px 8px",
|
|
5396
|
+
borderRadius: 4,
|
|
5397
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
5398
|
+
background: "transparent",
|
|
5399
|
+
color: "inherit",
|
|
5400
|
+
fontSize: 11,
|
|
5401
|
+
cursor: "pointer"
|
|
5402
|
+
};
|
|
5094
5403
|
const LIST_STYLE$2 = {
|
|
5095
5404
|
margin: 0,
|
|
5096
5405
|
padding: 0,
|
|
5097
5406
|
listStyle: "none"
|
|
5098
5407
|
};
|
|
5408
|
+
const CHECK_STYLE$1 = {
|
|
5409
|
+
marginTop: 4,
|
|
5410
|
+
flex: "0 0 auto"
|
|
5411
|
+
};
|
|
5099
5412
|
const ROW_STYLE = {
|
|
5100
5413
|
display: "flex",
|
|
5101
5414
|
alignItems: "flex-start",
|
|
5102
|
-
|
|
5103
|
-
gap: 8,
|
|
5415
|
+
gap: 6,
|
|
5104
5416
|
padding: "5px 0",
|
|
5105
5417
|
borderBottom: "1px solid rgba(127,127,127,0.1)"
|
|
5106
5418
|
};
|
|
@@ -5136,8 +5448,8 @@ const ROW_ACTIONS_STYLE = {
|
|
|
5136
5448
|
/**
|
|
5137
5449
|
* 任务「分组」tab:维护「为什么读」的分类,并把文献归入其中。
|
|
5138
5450
|
*
|
|
5139
|
-
*
|
|
5140
|
-
*
|
|
5451
|
+
* 分组是任务内概念;归组在结果卡片的标注区完成,这里负责增删改名、排序。
|
|
5452
|
+
* 增删与排序立即保存,名称和说明在输入框失焦时保存——切到结果页或别处不会丢改动。
|
|
5141
5453
|
*/
|
|
5142
5454
|
function draftOf(groups) {
|
|
5143
5455
|
return groups.map((group) => ({ ...group }));
|
|
@@ -5149,9 +5461,9 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5149
5461
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
5150
5462
|
const [error, setError] = (0, react.useState)(null);
|
|
5151
5463
|
const [toast, setToast] = (0, react.useState)("");
|
|
5152
|
-
(0, react.
|
|
5153
|
-
|
|
5154
|
-
|
|
5464
|
+
const latest = (0, react.useRef)(draft);
|
|
5465
|
+
const queued = (0, react.useRef)(false);
|
|
5466
|
+
const saving = (0, react.useRef)(false);
|
|
5155
5467
|
const usage = (0, react.useMemo)(() => {
|
|
5156
5468
|
const counts = /* @__PURE__ */ new Map();
|
|
5157
5469
|
for (const state of Object.values(detail.states)) {
|
|
@@ -5160,15 +5472,37 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5160
5472
|
}
|
|
5161
5473
|
return counts;
|
|
5162
5474
|
}, [detail.states]);
|
|
5163
|
-
const
|
|
5164
|
-
group
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5475
|
+
const persist = (0, react.useCallback)((next) => {
|
|
5476
|
+
latest.current = next.map((group, order) => ({
|
|
5477
|
+
...group,
|
|
5478
|
+
order
|
|
5479
|
+
}));
|
|
5480
|
+
queued.current = true;
|
|
5481
|
+
if (saving.current) return;
|
|
5482
|
+
saving.current = true;
|
|
5483
|
+
setBusy(true);
|
|
5484
|
+
setError(null);
|
|
5485
|
+
setToast("");
|
|
5486
|
+
(async () => {
|
|
5487
|
+
try {
|
|
5488
|
+
while (queued.current) {
|
|
5489
|
+
queued.current = false;
|
|
5490
|
+
await updateTask(taskId, { groups: latest.current.map((group) => ({ ...group })) });
|
|
5491
|
+
}
|
|
5492
|
+
setToast(t("groups.saved"));
|
|
5493
|
+
onSaved();
|
|
5494
|
+
} catch (reason) {
|
|
5495
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
5496
|
+
} finally {
|
|
5497
|
+
saving.current = false;
|
|
5498
|
+
setBusy(false);
|
|
5499
|
+
}
|
|
5500
|
+
})();
|
|
5501
|
+
}, [
|
|
5502
|
+
taskId,
|
|
5503
|
+
t,
|
|
5504
|
+
onSaved
|
|
5505
|
+
]);
|
|
5172
5506
|
const add = () => {
|
|
5173
5507
|
const trimmed = name.trim();
|
|
5174
5508
|
if (trimmed === "") return;
|
|
@@ -5179,43 +5513,30 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5179
5513
|
};
|
|
5180
5514
|
const desc = description.trim();
|
|
5181
5515
|
if (desc !== "") group.description = desc.slice(0, 200);
|
|
5182
|
-
|
|
5516
|
+
const next = [...draft, group];
|
|
5517
|
+
setDraft(next);
|
|
5183
5518
|
setName("");
|
|
5184
5519
|
setDescription("");
|
|
5185
|
-
|
|
5520
|
+
persist(next);
|
|
5186
5521
|
};
|
|
5187
5522
|
const move = (index, delta) => {
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5523
|
+
const target = index + delta;
|
|
5524
|
+
if (target < 0 || target >= draft.length) return;
|
|
5525
|
+
const next = [...draft];
|
|
5526
|
+
const [item] = next.splice(index, 1);
|
|
5527
|
+
next.splice(target, 0, item);
|
|
5528
|
+
const ordered = next.map((group, order) => ({
|
|
5529
|
+
...group,
|
|
5530
|
+
order
|
|
5531
|
+
}));
|
|
5532
|
+
setDraft(ordered);
|
|
5533
|
+
persist(ordered);
|
|
5199
5534
|
};
|
|
5200
5535
|
const remove = (group) => {
|
|
5201
5536
|
if (!window.confirm(tpl(t("groups.deleteConfirm"), { name: group.name }))) return;
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
if (busy) return;
|
|
5206
|
-
setBusy(true);
|
|
5207
|
-
setError(null);
|
|
5208
|
-
updateTask(taskId, { groups: draft.map((group, order) => ({
|
|
5209
|
-
...group,
|
|
5210
|
-
order
|
|
5211
|
-
})) }).then(() => {
|
|
5212
|
-
setToast(t("groups.saved"));
|
|
5213
|
-
onSaved();
|
|
5214
|
-
}).catch((reason) => {
|
|
5215
|
-
setError(reason instanceof Error ? reason.message : String(reason));
|
|
5216
|
-
}).finally(() => {
|
|
5217
|
-
setBusy(false);
|
|
5218
|
-
});
|
|
5537
|
+
const next = draft.filter((entry) => entry.id !== group.id);
|
|
5538
|
+
setDraft(next);
|
|
5539
|
+
persist(next);
|
|
5219
5540
|
};
|
|
5220
5541
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5221
5542
|
style: WRAP_STYLE$3,
|
|
@@ -5227,28 +5548,48 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5227
5548
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5228
5549
|
style: ADD_ROW_STYLE,
|
|
5229
5550
|
children: [
|
|
5230
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5231
|
-
style:
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5551
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5552
|
+
style: FIELD_STYLE$1,
|
|
5553
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5554
|
+
style: LABEL_STYLE,
|
|
5555
|
+
children: t("groups.name")
|
|
5556
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5557
|
+
style: INPUT_STYLE$2,
|
|
5558
|
+
value: name,
|
|
5559
|
+
placeholder: t("groups.namePlaceholder"),
|
|
5560
|
+
"aria-label": t("groups.name"),
|
|
5561
|
+
onChange: (event) => setName(event.target.value),
|
|
5562
|
+
onKeyDown: (event) => {
|
|
5563
|
+
if (event.key === "Enter") {
|
|
5564
|
+
event.preventDefault();
|
|
5565
|
+
add();
|
|
5566
|
+
}
|
|
5245
5567
|
}
|
|
5246
|
-
}
|
|
5568
|
+
})]
|
|
5247
5569
|
}),
|
|
5248
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5570
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5571
|
+
style: FIELD_STYLE$1,
|
|
5572
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5573
|
+
style: LABEL_STYLE,
|
|
5574
|
+
children: t("groups.desc")
|
|
5575
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5576
|
+
style: INPUT_STYLE$2,
|
|
5577
|
+
value: description,
|
|
5578
|
+
placeholder: t("groups.descPlaceholder"),
|
|
5579
|
+
"aria-label": t("groups.desc"),
|
|
5580
|
+
onChange: (event) => setDescription(event.target.value),
|
|
5581
|
+
onKeyDown: (event) => {
|
|
5582
|
+
if (event.key === "Enter") {
|
|
5583
|
+
event.preventDefault();
|
|
5584
|
+
add();
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
})]
|
|
5588
|
+
}),
|
|
5589
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
5590
|
+
variant: "outline",
|
|
5591
|
+
size: "sm",
|
|
5592
|
+
type: "button",
|
|
5252
5593
|
onClick: add,
|
|
5253
5594
|
children: t("groups.add")
|
|
5254
5595
|
})
|
|
@@ -5271,23 +5612,27 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5271
5612
|
fontWeight: 600
|
|
5272
5613
|
},
|
|
5273
5614
|
value: group.name,
|
|
5615
|
+
"aria-label": t("groups.name"),
|
|
5274
5616
|
onChange: (event) => setDraft((previous) => previous.map((entry) => entry.id === group.id ? {
|
|
5275
5617
|
...entry,
|
|
5276
5618
|
name: event.target.value
|
|
5277
|
-
} : entry))
|
|
5619
|
+
} : entry)),
|
|
5620
|
+
onBlur: () => persist(draft)
|
|
5278
5621
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5279
5622
|
style: COUNT_STYLE,
|
|
5280
5623
|
children: tpl(t("groups.count"), { n: usage.get(group.id) ?? 0 })
|
|
5281
5624
|
})]
|
|
5282
5625
|
}),
|
|
5283
5626
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5284
|
-
style:
|
|
5627
|
+
style: ROW_INPUT_STYLE,
|
|
5285
5628
|
value: group.description ?? "",
|
|
5286
5629
|
placeholder: t("groups.descPlaceholder"),
|
|
5630
|
+
"aria-label": t("groups.desc"),
|
|
5287
5631
|
onChange: (event) => setDraft((previous) => previous.map((entry) => entry.id === group.id ? {
|
|
5288
5632
|
...entry,
|
|
5289
5633
|
description: event.target.value
|
|
5290
|
-
} : entry))
|
|
5634
|
+
} : entry)),
|
|
5635
|
+
onBlur: () => persist(draft)
|
|
5291
5636
|
}),
|
|
5292
5637
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5293
5638
|
style: ACTION_ROW_STYLE$1,
|
|
@@ -5327,24 +5672,25 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5327
5672
|
}),
|
|
5328
5673
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5329
5674
|
style: ACTION_ROW_STYLE$1,
|
|
5330
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5331
|
-
variant: "outline",
|
|
5332
|
-
size: "sm",
|
|
5333
|
-
type: "button",
|
|
5334
|
-
disabled: !dirty || busy,
|
|
5335
|
-
onClick: save,
|
|
5336
|
-
children: t("groups.save")
|
|
5337
|
-
}), toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5675
|
+
children: [toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5338
5676
|
style: TOAST_STYLE$1,
|
|
5339
5677
|
children: toast
|
|
5678
|
+
}), busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5679
|
+
style: statusStyle,
|
|
5680
|
+
children: t("groups.saving")
|
|
5340
5681
|
})]
|
|
5341
5682
|
}),
|
|
5342
|
-
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5683
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5343
5684
|
style: {
|
|
5344
5685
|
...statusStyle,
|
|
5345
5686
|
color: "#d64545"
|
|
5346
5687
|
},
|
|
5347
|
-
children: error
|
|
5688
|
+
children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5689
|
+
type: "button",
|
|
5690
|
+
style: linkButtonStyle,
|
|
5691
|
+
onClick: () => persist(draft),
|
|
5692
|
+
children: t("groups.retry")
|
|
5693
|
+
})]
|
|
5348
5694
|
})
|
|
5349
5695
|
]
|
|
5350
5696
|
});
|
|
@@ -5359,6 +5705,18 @@ const ADD_ROW_STYLE = {
|
|
|
5359
5705
|
gap: 6,
|
|
5360
5706
|
flexWrap: "wrap"
|
|
5361
5707
|
};
|
|
5708
|
+
const FIELD_STYLE$1 = {
|
|
5709
|
+
display: "flex",
|
|
5710
|
+
alignItems: "center",
|
|
5711
|
+
gap: 4,
|
|
5712
|
+
flex: "1 1 140px",
|
|
5713
|
+
minWidth: 0
|
|
5714
|
+
};
|
|
5715
|
+
const LABEL_STYLE = {
|
|
5716
|
+
fontSize: 12,
|
|
5717
|
+
opacity: .7,
|
|
5718
|
+
whiteSpace: "nowrap"
|
|
5719
|
+
};
|
|
5362
5720
|
const INPUT_STYLE$2 = {
|
|
5363
5721
|
fontSize: 12,
|
|
5364
5722
|
padding: "3px 5px",
|
|
@@ -5374,6 +5732,10 @@ const LIST_STYLE$1 = {
|
|
|
5374
5732
|
flexDirection: "column",
|
|
5375
5733
|
gap: 6
|
|
5376
5734
|
};
|
|
5735
|
+
const ROW_INPUT_STYLE = {
|
|
5736
|
+
...INPUT_STYLE$2,
|
|
5737
|
+
flex: "0 0 auto"
|
|
5738
|
+
};
|
|
5377
5739
|
const GROUP_ROW_STYLE = {
|
|
5378
5740
|
display: "flex",
|
|
5379
5741
|
flexDirection: "column",
|
|
@@ -6050,6 +6412,8 @@ const CHIP_BUTTON_STYLE = {
|
|
|
6050
6412
|
* 备注 / 标签 / 必读顺序收在「标注」里,避免窄栏里堆满输入框。
|
|
6051
6413
|
*/
|
|
6052
6414
|
const ABSTRACT_PREVIEW = 260;
|
|
6415
|
+
/** 分组下拉里「去新建分组」的哨兵值,不会与真实 group id 冲突。 */
|
|
6416
|
+
const CREATE_GROUP_VALUE = "__create_group__";
|
|
6053
6417
|
const SCREENINGS$2 = [
|
|
6054
6418
|
"unread",
|
|
6055
6419
|
"read",
|
|
@@ -6062,7 +6426,7 @@ const SCREENING_COLOR = {
|
|
|
6062
6426
|
included: "#2e9e5b",
|
|
6063
6427
|
excluded: "#d64545"
|
|
6064
6428
|
};
|
|
6065
|
-
function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, onToggleSelect, onInsert, onAnnotate, onDownload, onZotero, onOpenPaper }) {
|
|
6429
|
+
function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, onToggleSelect, onInsert, onAnnotate, onDownload, onZotero, onOpenPaper, onManageGroups }) {
|
|
6066
6430
|
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
6067
6431
|
const [editing, setEditing] = (0, react.useState)(false);
|
|
6068
6432
|
const [entities, setEntities] = (0, react.useState)(false);
|
|
@@ -6111,63 +6475,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
6111
6475
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
6112
6476
|
style: cardStyle,
|
|
6113
6477
|
children: [
|
|
6114
|
-
paper.url === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6115
|
-
style: titleStyle,
|
|
6116
|
-
children: paper.title
|
|
6117
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6118
|
-
style: titleStyle,
|
|
6119
|
-
href: paper.url,
|
|
6120
|
-
target: "_blank",
|
|
6121
|
-
rel: "noreferrer noopener",
|
|
6122
|
-
children: paper.title
|
|
6123
|
-
}),
|
|
6124
|
-
meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6125
|
-
style: metaStyle,
|
|
6126
|
-
children: meta
|
|
6127
|
-
}),
|
|
6128
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6129
|
-
style: badgeRowStyle,
|
|
6130
|
-
children: [
|
|
6131
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6132
|
-
style: {
|
|
6133
|
-
...badgeStyle,
|
|
6134
|
-
color: SCREENING_COLOR[screening]
|
|
6135
|
-
},
|
|
6136
|
-
children: t(`screening.${screening}`)
|
|
6137
|
-
}),
|
|
6138
|
-
groupName !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6139
|
-
style: badgeStyle,
|
|
6140
|
-
children: groupName
|
|
6141
|
-
}),
|
|
6142
|
-
state?.shortlistOrder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6143
|
-
style: badgeStyle,
|
|
6144
|
-
children: tpl(t("annotate.shortlistBadge"), { n: state.shortlistOrder })
|
|
6145
|
-
}),
|
|
6146
|
-
sources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6147
|
-
style: badgeStyle,
|
|
6148
|
-
children: t(`source.${source}`)
|
|
6149
|
-
}, source)),
|
|
6150
|
-
paper.citedBy !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6151
|
-
style: badgeStyle,
|
|
6152
|
-
children: tpl(t("result.citedBy"), { n: paper.citedBy })
|
|
6153
|
-
}),
|
|
6154
|
-
paper.openAccess && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6155
|
-
style: badgeStyle,
|
|
6156
|
-
children: t("result.openAccess")
|
|
6157
|
-
}),
|
|
6158
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6159
|
-
style: {
|
|
6160
|
-
...badgeStyle,
|
|
6161
|
-
color: PDF_COLOR[pdfStatus]
|
|
6162
|
-
},
|
|
6163
|
-
children: t(`pdf.status.${pdfStatus}`)
|
|
6164
|
-
})
|
|
6165
|
-
]
|
|
6166
|
-
}),
|
|
6167
|
-
state?.pdfError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6168
|
-
style: PDF_ERROR_STYLE,
|
|
6169
|
-
children: tpl(t("pdf.error"), { message: state.pdfError })
|
|
6170
|
-
}),
|
|
6171
6478
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6172
6479
|
style: STATE_ROW_STYLE,
|
|
6173
6480
|
children: [
|
|
@@ -6194,19 +6501,35 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
6194
6501
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
6195
6502
|
style: SELECT_STYLE,
|
|
6196
6503
|
value: state?.groupId ?? "",
|
|
6197
|
-
disabled: busy
|
|
6504
|
+
disabled: busy,
|
|
6198
6505
|
"aria-label": t("annotate.group"),
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6506
|
+
title: groups.length === 0 ? t("annotate.groupHint") : t("annotate.group"),
|
|
6507
|
+
onChange: (event) => {
|
|
6508
|
+
const value = event.target.value;
|
|
6509
|
+
if (value === CREATE_GROUP_VALUE) {
|
|
6510
|
+
event.target.value = state?.groupId ?? "";
|
|
6511
|
+
onManageGroups?.();
|
|
6512
|
+
return;
|
|
6513
|
+
}
|
|
6514
|
+
onAnnotate({
|
|
6515
|
+
paperId: paper.paperId,
|
|
6516
|
+
groupId: value === "" ? null : value
|
|
6517
|
+
});
|
|
6518
|
+
},
|
|
6519
|
+
children: [
|
|
6520
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
6521
|
+
value: "",
|
|
6522
|
+
children: t("annotate.noGroup")
|
|
6523
|
+
}),
|
|
6524
|
+
groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
6525
|
+
value: group.id,
|
|
6526
|
+
children: group.name
|
|
6527
|
+
}, group.id)),
|
|
6528
|
+
groups.length === 0 && onManageGroups !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
6529
|
+
value: CREATE_GROUP_VALUE,
|
|
6530
|
+
children: t("annotate.createGroup")
|
|
6531
|
+
})
|
|
6532
|
+
]
|
|
6210
6533
|
}),
|
|
6211
6534
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
6212
6535
|
type: "button",
|
|
@@ -6267,14 +6590,201 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
6267
6590
|
})
|
|
6268
6591
|
]
|
|
6269
6592
|
}),
|
|
6593
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6594
|
+
style: FIELD_ROW_STYLE,
|
|
6595
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6596
|
+
style: FIELD_ICON_STYLE,
|
|
6597
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleIcon, {})
|
|
6598
|
+
}), paper.url === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6599
|
+
style: {
|
|
6600
|
+
...titleStyle,
|
|
6601
|
+
...FIELD_BODY_STYLE
|
|
6602
|
+
},
|
|
6603
|
+
children: paper.title
|
|
6604
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6605
|
+
style: {
|
|
6606
|
+
...titleStyle,
|
|
6607
|
+
...FIELD_BODY_STYLE
|
|
6608
|
+
},
|
|
6609
|
+
href: paper.url,
|
|
6610
|
+
target: "_blank",
|
|
6611
|
+
rel: "noreferrer noopener",
|
|
6612
|
+
children: paper.title
|
|
6613
|
+
})]
|
|
6614
|
+
}),
|
|
6615
|
+
meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6616
|
+
style: {
|
|
6617
|
+
...FIELD_ROW_STYLE,
|
|
6618
|
+
marginTop: 4
|
|
6619
|
+
},
|
|
6620
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6621
|
+
style: {
|
|
6622
|
+
...FIELD_ICON_STYLE,
|
|
6623
|
+
marginTop: 1
|
|
6624
|
+
},
|
|
6625
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuthorsIcon, {})
|
|
6626
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6627
|
+
style: {
|
|
6628
|
+
...metaStyle,
|
|
6629
|
+
...FIELD_BODY_STYLE,
|
|
6630
|
+
marginTop: 0
|
|
6631
|
+
},
|
|
6632
|
+
children: meta
|
|
6633
|
+
})]
|
|
6634
|
+
}),
|
|
6270
6635
|
paper.abstract !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6271
|
-
style:
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6636
|
+
style: {
|
|
6637
|
+
...FIELD_ROW_STYLE,
|
|
6638
|
+
marginTop: 6
|
|
6639
|
+
},
|
|
6640
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6641
|
+
style: FIELD_ICON_STYLE,
|
|
6642
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AbstractIcon, {})
|
|
6643
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6644
|
+
style: {
|
|
6645
|
+
...abstractStyle,
|
|
6646
|
+
...FIELD_BODY_STYLE,
|
|
6647
|
+
marginTop: 0
|
|
6648
|
+
},
|
|
6649
|
+
children: [abstract, canCollapse && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6650
|
+
type: "button",
|
|
6651
|
+
style: linkButtonStyle,
|
|
6652
|
+
onClick: () => setExpanded((value) => !value),
|
|
6653
|
+
children: expanded ? t("result.collapseAbstract") : t("result.expandAbstract")
|
|
6654
|
+
})] })]
|
|
6655
|
+
})]
|
|
6656
|
+
}),
|
|
6657
|
+
state?.pdfError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6658
|
+
style: PDF_ERROR_STYLE,
|
|
6659
|
+
children: tpl(t("pdf.error"), { message: state.pdfError })
|
|
6660
|
+
}),
|
|
6661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6662
|
+
style: CARD_ROW_STYLE,
|
|
6663
|
+
title: t("result.infoHint"),
|
|
6664
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6665
|
+
style: INFO_LABEL_STYLE,
|
|
6666
|
+
children: t("result.infoLabel")
|
|
6667
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6668
|
+
style: {
|
|
6669
|
+
...badgeRowStyle,
|
|
6670
|
+
...FIELD_BODY_STYLE,
|
|
6671
|
+
marginTop: 0
|
|
6672
|
+
},
|
|
6673
|
+
children: [
|
|
6674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6675
|
+
style: {
|
|
6676
|
+
...INFO_BADGE_STYLE,
|
|
6677
|
+
color: SCREENING_COLOR[screening]
|
|
6678
|
+
},
|
|
6679
|
+
children: t(`screening.${screening}`)
|
|
6680
|
+
}),
|
|
6681
|
+
groupName !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6682
|
+
style: INFO_BADGE_STYLE,
|
|
6683
|
+
children: groupName
|
|
6684
|
+
}),
|
|
6685
|
+
state?.shortlistOrder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6686
|
+
style: INFO_BADGE_STYLE,
|
|
6687
|
+
children: tpl(t("annotate.shortlistBadge"), { n: state.shortlistOrder })
|
|
6688
|
+
}),
|
|
6689
|
+
sources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6690
|
+
style: INFO_BADGE_STYLE,
|
|
6691
|
+
children: t(`source.${source}`)
|
|
6692
|
+
}, source)),
|
|
6693
|
+
paper.citedBy !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6694
|
+
style: INFO_BADGE_STYLE,
|
|
6695
|
+
children: tpl(t("result.citedBy"), { n: paper.citedBy })
|
|
6696
|
+
}),
|
|
6697
|
+
paper.openAccess && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6698
|
+
style: INFO_BADGE_STYLE,
|
|
6699
|
+
children: t("result.openAccess")
|
|
6700
|
+
}),
|
|
6701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6702
|
+
style: {
|
|
6703
|
+
...INFO_BADGE_STYLE,
|
|
6704
|
+
color: PDF_COLOR[pdfStatus]
|
|
6705
|
+
},
|
|
6706
|
+
children: t(`pdf.status.${pdfStatus}`)
|
|
6707
|
+
})
|
|
6708
|
+
]
|
|
6709
|
+
})]
|
|
6710
|
+
}),
|
|
6711
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6712
|
+
style: CARD_ROW_STYLE,
|
|
6713
|
+
title: t("result.actionHint"),
|
|
6714
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6715
|
+
style: ACTION_LABEL_STYLE,
|
|
6716
|
+
children: t("result.actionLabel")
|
|
6717
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6718
|
+
style: {
|
|
6719
|
+
...actionRowStyle,
|
|
6720
|
+
...FIELD_BODY_STYLE,
|
|
6721
|
+
marginTop: 0,
|
|
6722
|
+
flexWrap: "wrap",
|
|
6723
|
+
rowGap: 4
|
|
6724
|
+
},
|
|
6725
|
+
children: [
|
|
6726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
6727
|
+
variant: "ghost",
|
|
6728
|
+
size: "sm",
|
|
6729
|
+
type: "button",
|
|
6730
|
+
style: { color: ACCENT },
|
|
6731
|
+
onClick: () => onInsert(paper),
|
|
6732
|
+
children: t("result.insert")
|
|
6733
|
+
}),
|
|
6734
|
+
onOpenPaper !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6735
|
+
type: "button",
|
|
6736
|
+
style: linkButtonStyle,
|
|
6737
|
+
onClick: () => onOpenPaper(paper),
|
|
6738
|
+
children: t("paper.open")
|
|
6739
|
+
}),
|
|
6740
|
+
paper.ids.pmid !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6741
|
+
type: "button",
|
|
6742
|
+
style: linkButtonStyle,
|
|
6743
|
+
onClick: () => setEntities((value) => !value),
|
|
6744
|
+
children: t("entity.title")
|
|
6745
|
+
}),
|
|
6746
|
+
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6747
|
+
type: "button",
|
|
6748
|
+
style: linkButtonStyle,
|
|
6749
|
+
onClick: () => setPreview((value) => value === "pdf" ? null : "pdf"),
|
|
6750
|
+
children: t("pdf.preview")
|
|
6751
|
+
}),
|
|
6752
|
+
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6753
|
+
type: "button",
|
|
6754
|
+
style: linkButtonStyle,
|
|
6755
|
+
disabled: busy,
|
|
6756
|
+
onClick: () => onDownload(paper),
|
|
6757
|
+
children: t("pdf.download")
|
|
6758
|
+
}),
|
|
6759
|
+
canHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6760
|
+
type: "button",
|
|
6761
|
+
style: linkButtonStyle,
|
|
6762
|
+
onClick: () => setPreview((value) => value === "html" ? null : "html"),
|
|
6763
|
+
children: t("pdf.fulltext")
|
|
6764
|
+
}),
|
|
6765
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6766
|
+
type: "button",
|
|
6767
|
+
style: linkButtonStyle,
|
|
6768
|
+
disabled: busy,
|
|
6769
|
+
onClick: () => onZotero(paper),
|
|
6770
|
+
children: t("pdf.zotero")
|
|
6771
|
+
}),
|
|
6772
|
+
paper.pdfUrl !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6773
|
+
style: linkStyle,
|
|
6774
|
+
href: paper.pdfUrl,
|
|
6775
|
+
target: "_blank",
|
|
6776
|
+
rel: "noreferrer noopener",
|
|
6777
|
+
children: t("result.pdf")
|
|
6778
|
+
}),
|
|
6779
|
+
paper.doi !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6780
|
+
style: linkStyle,
|
|
6781
|
+
href: `https://doi.org/${paper.doi}`,
|
|
6782
|
+
target: "_blank",
|
|
6783
|
+
rel: "noreferrer noopener",
|
|
6784
|
+
children: "DOI"
|
|
6785
|
+
})
|
|
6786
|
+
]
|
|
6787
|
+
})]
|
|
6278
6788
|
}),
|
|
6279
6789
|
entities && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntityPanel, {
|
|
6280
6790
|
t,
|
|
@@ -6297,70 +6807,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
6297
6807
|
src: preview === "pdf" ? paperPdfUrl(taskId, paper.paperId) : paperFullTextUrl(taskId, paper.paperId),
|
|
6298
6808
|
style: IFRAME_STYLE
|
|
6299
6809
|
})]
|
|
6300
|
-
}),
|
|
6301
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6302
|
-
style: actionRowStyle,
|
|
6303
|
-
children: [
|
|
6304
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
6305
|
-
variant: "ghost",
|
|
6306
|
-
size: "sm",
|
|
6307
|
-
type: "button",
|
|
6308
|
-
onClick: () => onInsert(paper),
|
|
6309
|
-
children: t("result.insert")
|
|
6310
|
-
}),
|
|
6311
|
-
onOpenPaper !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6312
|
-
type: "button",
|
|
6313
|
-
style: linkButtonStyle,
|
|
6314
|
-
onClick: () => onOpenPaper(paper),
|
|
6315
|
-
children: t("paper.open")
|
|
6316
|
-
}),
|
|
6317
|
-
paper.ids.pmid !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6318
|
-
type: "button",
|
|
6319
|
-
style: linkButtonStyle,
|
|
6320
|
-
onClick: () => setEntities((value) => !value),
|
|
6321
|
-
children: t("entity.title")
|
|
6322
|
-
}),
|
|
6323
|
-
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6324
|
-
type: "button",
|
|
6325
|
-
style: linkButtonStyle,
|
|
6326
|
-
onClick: () => setPreview((value) => value === "pdf" ? null : "pdf"),
|
|
6327
|
-
children: t("pdf.preview")
|
|
6328
|
-
}),
|
|
6329
|
-
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6330
|
-
type: "button",
|
|
6331
|
-
style: linkButtonStyle,
|
|
6332
|
-
disabled: busy,
|
|
6333
|
-
onClick: () => onDownload(paper),
|
|
6334
|
-
children: t("pdf.download")
|
|
6335
|
-
}),
|
|
6336
|
-
canHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6337
|
-
type: "button",
|
|
6338
|
-
style: linkButtonStyle,
|
|
6339
|
-
onClick: () => setPreview((value) => value === "html" ? null : "html"),
|
|
6340
|
-
children: t("pdf.fulltext")
|
|
6341
|
-
}),
|
|
6342
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6343
|
-
type: "button",
|
|
6344
|
-
style: linkButtonStyle,
|
|
6345
|
-
disabled: busy,
|
|
6346
|
-
onClick: () => onZotero(paper),
|
|
6347
|
-
children: t("pdf.zotero")
|
|
6348
|
-
}),
|
|
6349
|
-
paper.pdfUrl !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6350
|
-
style: linkStyle,
|
|
6351
|
-
href: paper.pdfUrl,
|
|
6352
|
-
target: "_blank",
|
|
6353
|
-
rel: "noreferrer noopener",
|
|
6354
|
-
children: t("result.pdf")
|
|
6355
|
-
}),
|
|
6356
|
-
paper.doi !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
6357
|
-
style: linkStyle,
|
|
6358
|
-
href: `https://doi.org/${paper.doi}`,
|
|
6359
|
-
target: "_blank",
|
|
6360
|
-
rel: "noreferrer noopener",
|
|
6361
|
-
children: "DOI"
|
|
6362
|
-
})
|
|
6363
|
-
]
|
|
6364
6810
|
})
|
|
6365
6811
|
]
|
|
6366
6812
|
});
|
|
@@ -6401,14 +6847,66 @@ const IFRAME_STYLE = {
|
|
|
6401
6847
|
border: "none",
|
|
6402
6848
|
background: "#fff"
|
|
6403
6849
|
};
|
|
6850
|
+
/** 卡片顶部工具行:勾选 / 筛读 / 分组 / 标注。 */
|
|
6404
6851
|
const STATE_ROW_STYLE = {
|
|
6405
6852
|
display: "flex",
|
|
6406
6853
|
alignItems: "center",
|
|
6407
6854
|
flexWrap: "wrap",
|
|
6408
|
-
gap:
|
|
6409
|
-
|
|
6855
|
+
gap: 8,
|
|
6856
|
+
marginBottom: 8,
|
|
6410
6857
|
fontSize: 12
|
|
6411
6858
|
};
|
|
6859
|
+
/** 标题 / 作者 / 摘要统一为「图标 + 正文」两列,长文本只在正文列换行。 */
|
|
6860
|
+
const FIELD_ROW_STYLE = {
|
|
6861
|
+
display: "flex",
|
|
6862
|
+
alignItems: "flex-start",
|
|
6863
|
+
gap: 6
|
|
6864
|
+
};
|
|
6865
|
+
const FIELD_ICON_STYLE = {
|
|
6866
|
+
display: "inline-flex",
|
|
6867
|
+
flexShrink: 0,
|
|
6868
|
+
width: 14,
|
|
6869
|
+
marginTop: 2,
|
|
6870
|
+
color: ACCENT,
|
|
6871
|
+
opacity: .75
|
|
6872
|
+
};
|
|
6873
|
+
const FIELD_BODY_STYLE = {
|
|
6874
|
+
flex: 1,
|
|
6875
|
+
minWidth: 0
|
|
6876
|
+
};
|
|
6877
|
+
/**
|
|
6878
|
+
* 卡片底部两行:左边固定宽度的行标签,右边是内容。
|
|
6879
|
+
* 「状态与来源」用青绿、「快捷操作」用主题蓝,避免两行都是蓝字难以分辨。
|
|
6880
|
+
*/
|
|
6881
|
+
const INFO_COLOR = "#12836c";
|
|
6882
|
+
const CARD_ROW_STYLE = {
|
|
6883
|
+
display: "flex",
|
|
6884
|
+
alignItems: "flex-start",
|
|
6885
|
+
gap: 8,
|
|
6886
|
+
marginTop: 8
|
|
6887
|
+
};
|
|
6888
|
+
const ROW_LABEL_STYLE = {
|
|
6889
|
+
flexShrink: 0,
|
|
6890
|
+
width: 92,
|
|
6891
|
+
paddingTop: 1,
|
|
6892
|
+
fontSize: 11,
|
|
6893
|
+
lineHeight: "18px",
|
|
6894
|
+
fontWeight: 600,
|
|
6895
|
+
whiteSpace: "nowrap"
|
|
6896
|
+
};
|
|
6897
|
+
const INFO_LABEL_STYLE = {
|
|
6898
|
+
...ROW_LABEL_STYLE,
|
|
6899
|
+
color: INFO_COLOR
|
|
6900
|
+
};
|
|
6901
|
+
const ACTION_LABEL_STYLE = {
|
|
6902
|
+
...ROW_LABEL_STYLE,
|
|
6903
|
+
color: ACCENT
|
|
6904
|
+
};
|
|
6905
|
+
const INFO_BADGE_STYLE = {
|
|
6906
|
+
...badgeStyle,
|
|
6907
|
+
color: INFO_COLOR,
|
|
6908
|
+
border: "1px solid rgba(18,131,108,0.45)"
|
|
6909
|
+
};
|
|
6412
6910
|
const SELECT_STYLE = {
|
|
6413
6911
|
fontSize: 11,
|
|
6414
6912
|
padding: "2px 4px",
|
|
@@ -6422,7 +6920,7 @@ const EDIT_STYLE = {
|
|
|
6422
6920
|
display: "flex",
|
|
6423
6921
|
flexDirection: "column",
|
|
6424
6922
|
gap: 6,
|
|
6425
|
-
|
|
6923
|
+
marginBottom: 8,
|
|
6426
6924
|
padding: "6px 8px",
|
|
6427
6925
|
borderRadius: 6,
|
|
6428
6926
|
background: "rgba(127,127,127,0.07)"
|
|
@@ -6689,6 +7187,9 @@ const roundCardStyle = {
|
|
|
6689
7187
|
* 文本类条件(关键词、年份、类型)本地暂存 300ms 再上抛,避免每敲一个字都
|
|
6690
7188
|
* 打一次 Host;下拉类条件立即生效。父组件的筛选对象必须能直接喂给
|
|
6691
7189
|
* `/results` 与 `/stats`。
|
|
7190
|
+
*
|
|
7191
|
+
* 顶部标签与「收起筛选(N)」共用 collectActiveFilters:新增筛选维度时
|
|
7192
|
+
* 只在那里加一项,不要另外单写 chip,否则两处会再次不同步。
|
|
6692
7193
|
*/
|
|
6693
7194
|
const TEXT_DEBOUNCE_MS = 300;
|
|
6694
7195
|
const SORT_MODES = [
|
|
@@ -6696,11 +7197,7 @@ const SORT_MODES = [
|
|
|
6696
7197
|
"citations",
|
|
6697
7198
|
"year"
|
|
6698
7199
|
];
|
|
6699
|
-
const LANGUAGES = [
|
|
6700
|
-
"any",
|
|
6701
|
-
"eng",
|
|
6702
|
-
"chi"
|
|
6703
|
-
];
|
|
7200
|
+
const LANGUAGES = ["eng", "chi"];
|
|
6704
7201
|
const SCREENINGS$1 = [
|
|
6705
7202
|
"unread",
|
|
6706
7203
|
"read",
|
|
@@ -6729,25 +7226,54 @@ function mergeDraft(base, draft) {
|
|
|
6729
7226
|
next.yearTo = draft.yearTo.trim() === "" || !Number.isFinite(yearTo) || yearTo <= 0 ? void 0 : Math.floor(yearTo);
|
|
6730
7227
|
return next;
|
|
6731
7228
|
}
|
|
7229
|
+
/**
|
|
7230
|
+
* 已生效条件的唯一清单:折叠态的计数与顶部标签都从这里派生,
|
|
7231
|
+
* 新增筛选维度时只需加一项,两处必然同步(此前只有 6 个维度有标签)。
|
|
7232
|
+
* `t` 可省略——只计数时用占位翻译,文案不参与判断。
|
|
7233
|
+
*/
|
|
7234
|
+
function collectActiveFilters(value, t = (key) => key, queries = [], groups = []) {
|
|
7235
|
+
const list = [];
|
|
7236
|
+
const add = (key, label, clear) => {
|
|
7237
|
+
list.push({
|
|
7238
|
+
key,
|
|
7239
|
+
label,
|
|
7240
|
+
clear
|
|
7241
|
+
});
|
|
7242
|
+
};
|
|
7243
|
+
if (value.q !== void 0 && value.q.trim() !== "") add("q", tpl(t("filter.keywordChip"), { value: value.q }), { q: void 0 });
|
|
7244
|
+
if (value.source !== void 0) add("source", tpl(t("filter.sourceChip"), { value: t(`source.${value.source}`) }), { source: void 0 });
|
|
7245
|
+
if (value.yearFrom !== void 0 || value.yearTo !== void 0) {
|
|
7246
|
+
const from = value.yearFrom === void 0 ? "" : String(value.yearFrom);
|
|
7247
|
+
const to = value.yearTo === void 0 ? "" : String(value.yearTo);
|
|
7248
|
+
add("year", tpl(t("filter.yearChip"), { value: `${from}–${to}` }), {
|
|
7249
|
+
yearFrom: void 0,
|
|
7250
|
+
yearTo: void 0
|
|
7251
|
+
});
|
|
7252
|
+
}
|
|
7253
|
+
if (value.screening !== void 0) add("screening", tpl(t("filter.screeningChip"), { value: t(`screening.${value.screening}`) }), { screening: void 0 });
|
|
7254
|
+
if (value.queryId !== void 0) {
|
|
7255
|
+
const name = queries.find((query) => query.id === value.queryId)?.name ?? value.queryId;
|
|
7256
|
+
add("queryId", tpl(t("filter.queryChip"), { value: name }), { queryId: void 0 });
|
|
7257
|
+
}
|
|
7258
|
+
if (value.groupId !== void 0) {
|
|
7259
|
+
const name = groups.find((group) => group.id === value.groupId)?.name ?? value.groupId;
|
|
7260
|
+
add("groupId", tpl(t("filter.groupChip"), { value: name }), { groupId: void 0 });
|
|
7261
|
+
}
|
|
7262
|
+
if (value.language !== void 0 && value.language !== "any") add("language", tpl(t("filter.languageChip"), { value: t(`lang.${value.language}`) }), { language: void 0 });
|
|
7263
|
+
if (value.sort !== void 0 && value.sort !== "relevance") add("sort", tpl(t("filter.sortChip"), { value: t(`sort.${value.sort}`) }), { sort: void 0 });
|
|
7264
|
+
if (value.openAccessOnly === true) add("openAccessOnly", t("filter.openAccess"), { openAccessOnly: void 0 });
|
|
7265
|
+
if (value.hasPdf === true) add("hasPdf", t("filter.hasPdf"), { hasPdf: void 0 });
|
|
7266
|
+
if (value.missingFullText === true) add("missingFullText", t("filter.missingFullText"), { missingFullText: void 0 });
|
|
7267
|
+
if (value.pdfStatus !== void 0) add("pdfStatus", tpl(t("filter.pdfStatusChip"), { value: t(`pdf.status.${value.pdfStatus}`) }), { pdfStatus: void 0 });
|
|
7268
|
+
if (value.type !== void 0) add("type", tpl(t("filter.typeChip"), { value: value.type }), { type: void 0 });
|
|
7269
|
+
if (value.venue !== void 0) add("venue", tpl(t("filter.venueChip"), { value: value.venue }), { venue: void 0 });
|
|
7270
|
+
if (value.author !== void 0) add("author", tpl(t("filter.authorChip"), { value: value.author }), { author: void 0 });
|
|
7271
|
+
if (value.newOnly === true) add("newOnly", t("filter.newChip"), { newOnly: void 0 });
|
|
7272
|
+
return list;
|
|
7273
|
+
}
|
|
6732
7274
|
/** 已生效的筛选条件数量(用于折叠态提示)。 */
|
|
6733
7275
|
function activeFilterCount(value) {
|
|
6734
|
-
|
|
6735
|
-
if (value.q !== void 0 && value.q.trim() !== "") count += 1;
|
|
6736
|
-
if (value.source !== void 0) count += 1;
|
|
6737
|
-
if (value.yearFrom !== void 0 || value.yearTo !== void 0) count += 1;
|
|
6738
|
-
if (value.openAccessOnly === true) count += 1;
|
|
6739
|
-
if (value.hasPdf === true) count += 1;
|
|
6740
|
-
if (value.missingFullText === true) count += 1;
|
|
6741
|
-
if (value.screening !== void 0) count += 1;
|
|
6742
|
-
if (value.language !== void 0) count += 1;
|
|
6743
|
-
if (value.queryId !== void 0) count += 1;
|
|
6744
|
-
if (value.type !== void 0) count += 1;
|
|
6745
|
-
if (value.sort !== void 0) count += 1;
|
|
6746
|
-
if (value.groupId !== void 0) count += 1;
|
|
6747
|
-
if (value.venue !== void 0) count += 1;
|
|
6748
|
-
if (value.author !== void 0) count += 1;
|
|
6749
|
-
if (value.newOnly === true) count += 1;
|
|
6750
|
-
return count;
|
|
7276
|
+
return collectActiveFilters(value).length;
|
|
6751
7277
|
}
|
|
6752
7278
|
function TaskFilters({ t, value, sources, queries, groups = [], onChange }) {
|
|
6753
7279
|
const [open, setOpen] = (0, react.useState)(false);
|
|
@@ -6780,9 +7306,8 @@ function TaskFilters({ t, value, sources, queries, groups = [], onChange }) {
|
|
|
6780
7306
|
...next
|
|
6781
7307
|
});
|
|
6782
7308
|
};
|
|
6783
|
-
const
|
|
6784
|
-
const
|
|
6785
|
-
const groupName = value.groupId === void 0 ? "" : groups.find((group) => group.id === value.groupId)?.name ?? value.groupId;
|
|
7309
|
+
const chips = collectActiveFilters(value, t, queries, groups);
|
|
7310
|
+
const count = chips.length;
|
|
6786
7311
|
const setDraftValue = (key, text) => {
|
|
6787
7312
|
setDraft((previous) => ({
|
|
6788
7313
|
...previous,
|
|
@@ -6806,30 +7331,10 @@ function TaskFilters({ t, value, sources, queries, groups = [], onChange }) {
|
|
|
6806
7331
|
onClick: () => onChange({}),
|
|
6807
7332
|
children: t("filter.reset")
|
|
6808
7333
|
}),
|
|
6809
|
-
|
|
6810
|
-
label:
|
|
6811
|
-
onRemove: () => patch(
|
|
6812
|
-
})
|
|
6813
|
-
value.queryId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveChip, {
|
|
6814
|
-
label: tpl(t("filter.queryChip"), { value: queryName }),
|
|
6815
|
-
onRemove: () => patch({ queryId: void 0 })
|
|
6816
|
-
}),
|
|
6817
|
-
value.groupId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveChip, {
|
|
6818
|
-
label: tpl(t("filter.groupChip"), { value: groupName }),
|
|
6819
|
-
onRemove: () => patch({ groupId: void 0 })
|
|
6820
|
-
}),
|
|
6821
|
-
value.venue !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveChip, {
|
|
6822
|
-
label: tpl(t("filter.venueChip"), { value: value.venue }),
|
|
6823
|
-
onRemove: () => patch({ venue: void 0 })
|
|
6824
|
-
}),
|
|
6825
|
-
value.author !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveChip, {
|
|
6826
|
-
label: tpl(t("filter.authorChip"), { value: value.author }),
|
|
6827
|
-
onRemove: () => patch({ author: void 0 })
|
|
6828
|
-
}),
|
|
6829
|
-
value.newOnly === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveChip, {
|
|
6830
|
-
label: t("filter.newChip"),
|
|
6831
|
-
onRemove: () => patch({ newOnly: void 0 })
|
|
6832
|
-
})
|
|
7334
|
+
chips.map((chip) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveChip, {
|
|
7335
|
+
label: chip.label,
|
|
7336
|
+
onRemove: () => patch(chip.clear)
|
|
7337
|
+
}, chip.key))
|
|
6833
7338
|
]
|
|
6834
7339
|
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6835
7340
|
style: GRID_STYLE,
|
|
@@ -6924,7 +7429,7 @@ function TaskFilters({ t, value, sources, queries, groups = [], onChange }) {
|
|
|
6924
7429
|
style: FIELD_STYLE,
|
|
6925
7430
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("filter.language") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
6926
7431
|
style: INPUT_STYLE,
|
|
6927
|
-
value: value.language ?? "",
|
|
7432
|
+
value: value.language === "any" ? "" : value.language ?? "",
|
|
6928
7433
|
onChange: (event) => patch({ language: event.target.value === "" ? void 0 : event.target.value }),
|
|
6929
7434
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
6930
7435
|
value: "",
|
|
@@ -6940,7 +7445,7 @@ function TaskFilters({ t, value, sources, queries, groups = [], onChange }) {
|
|
|
6940
7445
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("filter.sort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
6941
7446
|
style: INPUT_STYLE,
|
|
6942
7447
|
value: value.sort ?? "relevance",
|
|
6943
|
-
onChange: (event) => patch({ sort: event.target.value }),
|
|
7448
|
+
onChange: (event) => patch({ sort: event.target.value === "relevance" ? void 0 : event.target.value }),
|
|
6944
7449
|
children: SORT_MODES.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
6945
7450
|
value: mode,
|
|
6946
7451
|
children: t(`sort.${mode}`)
|
|
@@ -7350,7 +7855,7 @@ function formatDate(value) {
|
|
|
7350
7855
|
const date = new Date(value);
|
|
7351
7856
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
7352
7857
|
}
|
|
7353
|
-
function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
7858
|
+
function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onCloseTask }) {
|
|
7354
7859
|
const info = useTabInfo();
|
|
7355
7860
|
const taskId = parseTaskId(info);
|
|
7356
7861
|
const [filters, setFilters] = (0, react.useState)({});
|
|
@@ -7362,6 +7867,8 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7362
7867
|
const [risFallback, setRisFallback] = (0, react.useState)(null);
|
|
7363
7868
|
const [showRounds, setShowRounds] = (0, react.useState)(false);
|
|
7364
7869
|
const [showTaskList, setShowTaskList] = (0, react.useState)(false);
|
|
7870
|
+
/** 任务级全文统计:结果页覆盖率横幅与全文页台账共用同一份数据。 */
|
|
7871
|
+
const [overview, setOverview] = (0, react.useState)(null);
|
|
7365
7872
|
const detail = view.detail;
|
|
7366
7873
|
const filterActive = (0, react.useMemo)(() => activeFilterCount(filters) > 0, [filters]);
|
|
7367
7874
|
const filterKey = JSON.stringify(filters);
|
|
@@ -7375,6 +7882,19 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7375
7882
|
(0, react.useEffect)(() => {
|
|
7376
7883
|
if (detail !== null) rememberTaskTitle(detail.id, detail.title);
|
|
7377
7884
|
}, [detail]);
|
|
7885
|
+
(0, react.useEffect)(() => {
|
|
7886
|
+
if (taskId === null) return void 0;
|
|
7887
|
+
const controller = new AbortController();
|
|
7888
|
+
fetchStats(taskId, {}, "task", controller.signal).then((report) => {
|
|
7889
|
+
if (!controller.signal.aborted) setOverview(report);
|
|
7890
|
+
}).catch(() => {
|
|
7891
|
+
if (!controller.signal.aborted) setOverview(null);
|
|
7892
|
+
});
|
|
7893
|
+
return () => controller.abort();
|
|
7894
|
+
}, [taskId, detail?.rev]);
|
|
7895
|
+
(0, react.useEffect)(() => {
|
|
7896
|
+
setOverview(null);
|
|
7897
|
+
}, [taskId]);
|
|
7378
7898
|
(0, react.useEffect)(() => {
|
|
7379
7899
|
if (toast === "") return void 0;
|
|
7380
7900
|
const timer = setTimeout(() => setToast(""), 2400);
|
|
@@ -7398,6 +7918,15 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7398
7918
|
else setToast(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
7399
7919
|
})();
|
|
7400
7920
|
};
|
|
7921
|
+
/** 把任务 id 与当前条件带入会话草稿,新会话也能续接同一任务。 */
|
|
7922
|
+
const insertTask = () => {
|
|
7923
|
+
if (detail === null) return;
|
|
7924
|
+
(async () => {
|
|
7925
|
+
const outcome = await insertBuilt(inputActions, () => taskText(t, detail));
|
|
7926
|
+
if (outcome === "inserted") setToast(t("task.inserted"));
|
|
7927
|
+
else setToast(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
7928
|
+
})();
|
|
7929
|
+
};
|
|
7401
7930
|
const downloadPaper = (paper) => {
|
|
7402
7931
|
if (taskId === null) return;
|
|
7403
7932
|
(async () => {
|
|
@@ -7456,12 +7985,13 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7456
7985
|
})));
|
|
7457
7986
|
})();
|
|
7458
7987
|
};
|
|
7459
|
-
|
|
7460
|
-
|
|
7988
|
+
/** 结果页与全文页共用同一套批量下载 / Zotero 逻辑。 */
|
|
7989
|
+
const downloadIds = (paperIds) => {
|
|
7990
|
+
if (taskId === null || paperIds.length === 0) return;
|
|
7461
7991
|
(async () => {
|
|
7462
7992
|
let ok = 0;
|
|
7463
7993
|
let failed = 0;
|
|
7464
|
-
for (const paperId of
|
|
7994
|
+
for (const paperId of paperIds) try {
|
|
7465
7995
|
const result = await downloadPaperPdf(taskId, paperId);
|
|
7466
7996
|
if (result.status === "downloaded" || result.status === "html-only") ok += 1;
|
|
7467
7997
|
else failed += 1;
|
|
@@ -7475,23 +8005,25 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7475
8005
|
view.reload();
|
|
7476
8006
|
})();
|
|
7477
8007
|
};
|
|
7478
|
-
const
|
|
7479
|
-
if (taskId === null ||
|
|
7480
|
-
syncZotero(taskId,
|
|
8008
|
+
const zoteroIds = (paperIds) => {
|
|
8009
|
+
if (taskId === null || paperIds.length === 0) return;
|
|
8010
|
+
syncZotero(taskId, paperIds).then((result) => {
|
|
7481
8011
|
setToast(zoteroToast(t, result.saved, result.failed, result.messages));
|
|
7482
8012
|
if (result.failed > 0) setRisFallback({
|
|
7483
|
-
ids,
|
|
8013
|
+
ids: [...paperIds],
|
|
7484
8014
|
message: result.messages[0] ?? t("pdf.zoteroFailed")
|
|
7485
8015
|
});
|
|
7486
8016
|
}).catch((error) => {
|
|
7487
8017
|
const message = messageOf(error);
|
|
7488
8018
|
setToast(tpl(t("pdf.zoteroFailed"), { message }));
|
|
7489
8019
|
setRisFallback({
|
|
7490
|
-
ids,
|
|
8020
|
+
ids: [...paperIds],
|
|
7491
8021
|
message
|
|
7492
8022
|
});
|
|
7493
8023
|
});
|
|
7494
8024
|
};
|
|
8025
|
+
const downloadMany = () => downloadIds(ids);
|
|
8026
|
+
const zoteroMany = () => zoteroIds(ids);
|
|
7495
8027
|
if (taskId === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7496
8028
|
style: panelStyle,
|
|
7497
8029
|
children: t("task.notFound")
|
|
@@ -7519,6 +8051,10 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7519
8051
|
const failed = detail.result.sources.filter((source) => !source.ok && !source.skipped);
|
|
7520
8052
|
const skipped = detail.result.sources.filter((source) => source.skipped);
|
|
7521
8053
|
const page = view.page;
|
|
8054
|
+
const coverage = overview?.fullText;
|
|
8055
|
+
const overviewAvailable = coverage === void 0 ? 0 : coverage.available + coverage.downloaded + coverage.htmlOnly;
|
|
8056
|
+
const overviewMissing = coverage === void 0 ? 0 : coverage.unknown + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
8057
|
+
const overviewRate = overview === null || overview.loaded === 0 ? 0 : Math.round(overviewAvailable / overview.loaded * 100);
|
|
7522
8058
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7523
8059
|
style: panelStyle,
|
|
7524
8060
|
children: [
|
|
@@ -7663,6 +8199,14 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7663
8199
|
},
|
|
7664
8200
|
children: t("task.rerun")
|
|
7665
8201
|
}),
|
|
8202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
8203
|
+
variant: "outline",
|
|
8204
|
+
size: "sm",
|
|
8205
|
+
type: "button",
|
|
8206
|
+
title: t("task.insertToChatHint"),
|
|
8207
|
+
onClick: insertTask,
|
|
8208
|
+
children: t("task.insertToChat")
|
|
8209
|
+
}),
|
|
7666
8210
|
active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
7667
8211
|
variant: "ghost",
|
|
7668
8212
|
size: "sm",
|
|
@@ -7755,6 +8299,38 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7755
8299
|
]
|
|
7756
8300
|
}),
|
|
7757
8301
|
tab === "results" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
8302
|
+
overview !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8303
|
+
style: COVERAGE_WRAP_STYLE,
|
|
8304
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8305
|
+
style: COVERAGE_ROW_STYLE,
|
|
8306
|
+
children: [
|
|
8307
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("fulltext.coverageBar"), {
|
|
8308
|
+
available: overviewAvailable,
|
|
8309
|
+
total: overview.loaded,
|
|
8310
|
+
rate: overviewRate,
|
|
8311
|
+
missing: overviewMissing
|
|
8312
|
+
}) }),
|
|
8313
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8314
|
+
type: "button",
|
|
8315
|
+
style: filters.missingFullText === true ? QUICK_FILTER_ACTIVE_STYLE : QUICK_FILTER_STYLE,
|
|
8316
|
+
onClick: () => setFilters((previous) => ({
|
|
8317
|
+
...previous,
|
|
8318
|
+
missingFullText: previous.missingFullText === true ? void 0 : true
|
|
8319
|
+
})),
|
|
8320
|
+
children: t("fulltext.quickFilter")
|
|
8321
|
+
}),
|
|
8322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8323
|
+
type: "button",
|
|
8324
|
+
style: linkButtonStyle,
|
|
8325
|
+
onClick: () => setTab("fulltext"),
|
|
8326
|
+
children: t("fulltext.openTab")
|
|
8327
|
+
})
|
|
8328
|
+
]
|
|
8329
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8330
|
+
style: COVERAGE_HINT_STYLE,
|
|
8331
|
+
children: t("fulltext.barHint")
|
|
8332
|
+
})]
|
|
8333
|
+
}),
|
|
7758
8334
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFilters, {
|
|
7759
8335
|
t,
|
|
7760
8336
|
value: filters,
|
|
@@ -7803,6 +8379,7 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7803
8379
|
},
|
|
7804
8380
|
onDownload: downloadPaper,
|
|
7805
8381
|
onZotero: zoteroPaper,
|
|
8382
|
+
onManageGroups: () => setTab("groups"),
|
|
7806
8383
|
...onOpenPaper === void 0 ? {} : { onOpenPaper: (item) => onOpenPaper(taskId, item.paperId, item.title) }
|
|
7807
8384
|
}) }, paper.paperId))
|
|
7808
8385
|
}), page.papers.length < page.total && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -7842,13 +8419,13 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7842
8419
|
taskId,
|
|
7843
8420
|
rev: detail.rev,
|
|
7844
8421
|
states: detail.states,
|
|
8422
|
+
stats: overview,
|
|
7845
8423
|
onDownload: downloadPaper,
|
|
7846
8424
|
onZotero: zoteroPaper,
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
}));
|
|
8425
|
+
onBatchDownload: (papers) => downloadIds(papers.map((paper) => paper.paperId)),
|
|
8426
|
+
onBatchZotero: (papers) => zoteroIds(papers.map((paper) => paper.paperId)),
|
|
8427
|
+
onShowInResults: (viewKey) => {
|
|
8428
|
+
setFilters(viewKey === "missing" ? { missingFullText: true } : { pdfStatus: viewKey });
|
|
7852
8429
|
setTab("results");
|
|
7853
8430
|
}
|
|
7854
8431
|
}) : tab === "groups" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupsTab, {
|
|
@@ -7900,7 +8477,11 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask }) {
|
|
|
7900
8477
|
open: showTaskList,
|
|
7901
8478
|
onClose: () => setShowTaskList(false),
|
|
7902
8479
|
currentTaskId: taskId,
|
|
7903
|
-
onOpenTask: onOpenTask ?? (() => void 0)
|
|
8480
|
+
onOpenTask: onOpenTask ?? (() => void 0),
|
|
8481
|
+
onCurrentRemoved: (fallback) => {
|
|
8482
|
+
if (fallback !== null) onOpenTask?.(fallback.id, fallback.title);
|
|
8483
|
+
else onCloseTask?.(taskId);
|
|
8484
|
+
}
|
|
7904
8485
|
})
|
|
7905
8486
|
]
|
|
7906
8487
|
});
|
|
@@ -8018,8 +8599,9 @@ function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, filters, onClea
|
|
|
8018
8599
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
8019
8600
|
style: BULK_SELECT_STYLE,
|
|
8020
8601
|
value: "",
|
|
8021
|
-
disabled: busy
|
|
8602
|
+
disabled: busy,
|
|
8022
8603
|
"aria-label": t("bulk.setGroup"),
|
|
8604
|
+
title: groups.length === 0 ? t("annotate.groupHint") : t("bulk.setGroup"),
|
|
8023
8605
|
onChange: (event) => {
|
|
8024
8606
|
const value = event.target.value;
|
|
8025
8607
|
if (value !== "") onAnnotate({ groupId: value === "__none__" ? null : value });
|
|
@@ -8145,8 +8727,10 @@ const TAB_ACTIVE_STYLE = {
|
|
|
8145
8727
|
};
|
|
8146
8728
|
const SCREENING_BAR_STYLE = {
|
|
8147
8729
|
display: "flex",
|
|
8148
|
-
|
|
8149
|
-
|
|
8730
|
+
alignItems: "center",
|
|
8731
|
+
justifyContent: "space-between",
|
|
8732
|
+
flexWrap: "wrap",
|
|
8733
|
+
gap: 8,
|
|
8150
8734
|
padding: "2px 0 4px",
|
|
8151
8735
|
borderBottom: "1px solid rgba(127,127,127,0.12)"
|
|
8152
8736
|
};
|
|
@@ -8174,8 +8758,10 @@ const SCREENING_PILL_ACTIVE = {
|
|
|
8174
8758
|
const EXPORT_ROW_STYLE = {
|
|
8175
8759
|
display: "flex",
|
|
8176
8760
|
alignItems: "center",
|
|
8177
|
-
flexWrap: "
|
|
8761
|
+
flexWrap: "nowrap",
|
|
8762
|
+
whiteSpace: "nowrap",
|
|
8178
8763
|
gap: 8,
|
|
8764
|
+
marginLeft: "auto",
|
|
8179
8765
|
fontSize: 11
|
|
8180
8766
|
};
|
|
8181
8767
|
const EXPORT_LABEL_STYLE = { opacity: .66 };
|
|
@@ -8208,6 +8794,41 @@ const BULK_SELECT_STYLE = {
|
|
|
8208
8794
|
color: "inherit",
|
|
8209
8795
|
maxWidth: 140
|
|
8210
8796
|
};
|
|
8797
|
+
const COVERAGE_WRAP_STYLE = {
|
|
8798
|
+
display: "flex",
|
|
8799
|
+
flexDirection: "column",
|
|
8800
|
+
gap: 2,
|
|
8801
|
+
padding: "5px 8px",
|
|
8802
|
+
borderRadius: 6,
|
|
8803
|
+
background: "rgba(80,140,255,0.08)",
|
|
8804
|
+
border: "1px solid rgba(80,140,255,0.25)",
|
|
8805
|
+
fontSize: 12
|
|
8806
|
+
};
|
|
8807
|
+
const COVERAGE_ROW_STYLE = {
|
|
8808
|
+
display: "flex",
|
|
8809
|
+
alignItems: "center",
|
|
8810
|
+
flexWrap: "wrap",
|
|
8811
|
+
gap: 8
|
|
8812
|
+
};
|
|
8813
|
+
const COVERAGE_HINT_STYLE = {
|
|
8814
|
+
fontSize: 11,
|
|
8815
|
+
opacity: .66
|
|
8816
|
+
};
|
|
8817
|
+
const QUICK_FILTER_STYLE = {
|
|
8818
|
+
padding: "1px 7px",
|
|
8819
|
+
borderRadius: 10,
|
|
8820
|
+
border: "1px solid rgba(127,127,127,0.3)",
|
|
8821
|
+
background: "transparent",
|
|
8822
|
+
color: "inherit",
|
|
8823
|
+
fontSize: 11,
|
|
8824
|
+
cursor: "pointer"
|
|
8825
|
+
};
|
|
8826
|
+
const QUICK_FILTER_ACTIVE_STYLE = {
|
|
8827
|
+
...QUICK_FILTER_STYLE,
|
|
8828
|
+
borderColor: "#508cff",
|
|
8829
|
+
color: "#508cff",
|
|
8830
|
+
fontWeight: 600
|
|
8831
|
+
};
|
|
8211
8832
|
// src/client/index.ts
|
|
8212
8833
|
/**
|
|
8213
8834
|
* dsh-plugin-litsearch —— 客户端入口。
|
|
@@ -8268,17 +8889,32 @@ function apply(ctx) {
|
|
|
8268
8889
|
priority: "extension",
|
|
8269
8890
|
keepMounted: true
|
|
8270
8891
|
}), "dsh-plugin-litsearch: paper tab");
|
|
8892
|
+
/** 当前会话里已打开的任务 tab。 */
|
|
8893
|
+
const taskTabsIn = (sessionId) => ctx.sidebarRight.tabsIn(sessionId).filter((tab) => tab.kind === TASK_TAB_ID);
|
|
8271
8894
|
/** 打开 / 聚焦某任务的 tab;先把标题记进缓存,布局恢复后不靠 params 也能显示。 */
|
|
8272
8895
|
const openTask = (id, title) => {
|
|
8273
8896
|
const address = `${TASK_ADDRESS_PREFIX}${encodeURIComponent(id)}`;
|
|
8274
8897
|
if (title !== void 0 && title !== "") rememberTaskTitle(id, title);
|
|
8275
8898
|
activateTask(id).catch(() => void 0);
|
|
8276
|
-
ctx.sidebarRight.
|
|
8899
|
+
const sessionId = ctx.sidebarRight.mounted.getSnapshot();
|
|
8900
|
+
const tabs = sessionId === void 0 ? [] : taskTabsIn(sessionId);
|
|
8901
|
+
const previous = tabs.find((tab) => taskIdFromAddress(tab.contentId) !== id);
|
|
8902
|
+
ctx.sidebarRight.openResource(address, {
|
|
8903
|
+
...title === void 0 || title === "" ? {} : { params: { title } },
|
|
8904
|
+
...previous === void 0 ? {} : { replaceTab: previous.id }
|
|
8905
|
+
});
|
|
8906
|
+
for (const tab of tabs) if (tab.id !== previous?.id && taskIdFromAddress(tab.contentId) !== id) ctx.sidebarRight.close(tab.id);
|
|
8277
8907
|
};
|
|
8278
8908
|
const Body = () => (0, react.createElement)(LitsearchPage, {
|
|
8279
8909
|
t,
|
|
8280
8910
|
onOpenTask: (id, title) => openTask(id, title)
|
|
8281
8911
|
});
|
|
8912
|
+
/** 关闭某个任务 tab;任务被删除且没有可切换的任务时用来清空窗口。 */
|
|
8913
|
+
const closeTask = (id) => {
|
|
8914
|
+
const sessionId = ctx.sidebarRight.mounted.getSnapshot();
|
|
8915
|
+
if (sessionId === void 0) return;
|
|
8916
|
+
for (const tab of taskTabsIn(sessionId)) if (taskIdFromAddress(tab.contentId) === id) ctx.sidebarRight.close(tab.id);
|
|
8917
|
+
};
|
|
8282
8918
|
ctx.effect(() => ctx.slots.register({
|
|
8283
8919
|
name: "sidebar.right.pane.tab",
|
|
8284
8920
|
key: TAB_ID
|
|
@@ -8293,7 +8929,8 @@ function apply(ctx) {
|
|
|
8293
8929
|
useTabInfo: props.useTabInfo,
|
|
8294
8930
|
inputActions: props.inputActions,
|
|
8295
8931
|
onOpenPaper: openPaper,
|
|
8296
|
-
onOpenTask: (id, title) => openTask(id, title)
|
|
8932
|
+
onOpenTask: (id, title) => openTask(id, title),
|
|
8933
|
+
onCloseTask: (id) => closeTask(id)
|
|
8297
8934
|
});
|
|
8298
8935
|
/** tab 标题:按地址里的任务 id 读缓存,缺了补取一次详情。 */
|
|
8299
8936
|
const TaskTitle = (props) => {
|
|
@@ -8359,7 +8996,28 @@ function apply(ctx) {
|
|
|
8359
8996
|
locale: NS,
|
|
8360
8997
|
inject: () => controller.inject()
|
|
8361
8998
|
}, SettingsSection))), "dsh-plugin-litsearch: settings section");
|
|
8362
|
-
ctx.effect(() => startAutoOpen({
|
|
8999
|
+
ctx.effect(() => startAutoOpen({
|
|
9000
|
+
open: (id, title) => openTask(id, title),
|
|
9001
|
+
sessionId: () => ctx.sidebarRight.mounted.getSnapshot()
|
|
9002
|
+
}), "dsh-plugin-litsearch: auto open");
|
|
9003
|
+
ctx.effect(() => {
|
|
9004
|
+
const sync = () => {
|
|
9005
|
+
const sessionId = ctx.sidebarRight.mounted.getSnapshot();
|
|
9006
|
+
if (sessionId === void 0) return;
|
|
9007
|
+
const tabs = taskTabsIn(sessionId);
|
|
9008
|
+
if (tabs.length === 0) return;
|
|
9009
|
+
const activeId = ctx.sidebarRight.active()?.id;
|
|
9010
|
+
const tab = tabs.find((entry) => entry.id === activeId) ?? tabs[0];
|
|
9011
|
+
for (const extra of tabs) if (extra.id !== tab.id) ctx.sidebarRight.close(extra.id);
|
|
9012
|
+
const id = taskIdFromAddress(tab.contentId);
|
|
9013
|
+
if (id !== null) activateTask(id).catch(() => void 0);
|
|
9014
|
+
};
|
|
9015
|
+
const unsubscribe = ctx.sidebarRight.mounted.subscribe(sync);
|
|
9016
|
+
sync();
|
|
9017
|
+
return () => {
|
|
9018
|
+
unsubscribe();
|
|
9019
|
+
};
|
|
9020
|
+
}, "dsh-plugin-litsearch: current task follows conversation");
|
|
8363
9021
|
ctx.effect(() => {
|
|
8364
9022
|
refreshHealth();
|
|
8365
9023
|
const timer = setInterval(() => {
|