dsh-plugin-litsearch 0.3.0 → 0.4.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 +158 -41
- package/client/client.js +2632 -926
- package/lib/host/events.d.ts +9 -1
- package/lib/host/events.js +50 -3
- package/lib/host/export/bibtex.js +1 -4
- package/lib/host/export/common.d.ts +0 -2
- package/lib/host/export/common.js +0 -7
- package/lib/host/export/csv.js +1 -5
- package/lib/host/export/markdown.d.ts +1 -1
- package/lib/host/export/markdown.js +9 -26
- package/lib/host/export/ris.js +1 -4
- package/lib/host/paths.d.ts +1 -1
- package/lib/host/paths.js +1 -1
- package/lib/host/pdf/store.d.ts +5 -3
- package/lib/host/pdf/store.js +50 -17
- package/lib/host/router.js +146 -79
- package/lib/host/tasks/download.d.ts +104 -0
- package/lib/host/tasks/download.js +274 -0
- package/lib/host/tasks/filter.d.ts +8 -1
- package/lib/host/tasks/filter.js +14 -0
- package/lib/host/tasks/service.d.ts +44 -11
- package/lib/host/tasks/service.js +216 -48
- package/lib/host/tasks/states.d.ts +1 -3
- package/lib/host/tasks/states.js +2 -66
- package/lib/host/tasks/stats.js +3 -0
- package/lib/host/tasks/summary.js +1 -1
- package/lib/host/tools.d.ts +5 -1
- package/lib/host/tools.js +813 -37
- package/lib/host/zotero/connector.d.ts +19 -10
- package/lib/host/zotero/connector.js +142 -39
- package/lib/host/zotero/match.d.ts +27 -0
- package/lib/host/zotero/match.js +59 -0
- package/lib/search/config.d.ts +0 -2
- package/lib/search/config.js +0 -2
- package/lib/search/oa.d.ts +8 -0
- package/lib/search/oa.js +53 -6
- package/lib/search/types.d.ts +154 -10
- package/lib/search/types.js +11 -0
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -41,6 +41,10 @@ const TASK_LANGUAGES = [
|
|
|
41
41
|
"ita",
|
|
42
42
|
"por"
|
|
43
43
|
];
|
|
44
|
+
/** 是否需要引导用户到浏览器手动保存。 */
|
|
45
|
+
function isManualPdfFailure(reason) {
|
|
46
|
+
return reason !== void 0 && reason !== "failed";
|
|
47
|
+
}
|
|
44
48
|
SOURCE_IDS.filter((id) => id !== "googlescholar");
|
|
45
49
|
/** 写端点的自定义请求头(与 JSON content-type 一起构成非简单请求)。 */
|
|
46
50
|
const WRITE_HEADER = "x-dsh-litsearch-write";
|
|
@@ -114,6 +118,10 @@ function fetchHealth(signal) {
|
|
|
114
118
|
function fetchEvents(sinceRev, signal) {
|
|
115
119
|
return request(`/events?sinceRev=${encodeURIComponent(String(sinceRev))}`, { signal });
|
|
116
120
|
}
|
|
121
|
+
/** 面板把「当前页 / 当前筛选」告诉 Host,模型工具才能准确解析下载范围。 */
|
|
122
|
+
function postViewSnapshot(body) {
|
|
123
|
+
return request("/view", writeInit("POST", body));
|
|
124
|
+
}
|
|
117
125
|
function listTasks(signal) {
|
|
118
126
|
return request("/tasks", { signal });
|
|
119
127
|
}
|
|
@@ -188,7 +196,7 @@ function probeSource(source, signal) {
|
|
|
188
196
|
signal
|
|
189
197
|
});
|
|
190
198
|
}
|
|
191
|
-
/** 批量写入筛读 /
|
|
199
|
+
/** 批量写入筛读 / 分组。 */
|
|
192
200
|
function annotateTask(id, papers, rev, signal) {
|
|
193
201
|
const body = { papers };
|
|
194
202
|
if (rev !== void 0) body.rev = rev;
|
|
@@ -219,6 +227,10 @@ function probeStorage() {
|
|
|
219
227
|
function probeZotero() {
|
|
220
228
|
return request("/settings/zotero", writeInit("POST", {}));
|
|
221
229
|
}
|
|
230
|
+
/** 读取可写入的 Zotero 库与分类。 */
|
|
231
|
+
function fetchZoteroTargets() {
|
|
232
|
+
return request("/settings/zotero/targets", writeInit("POST", {}));
|
|
233
|
+
}
|
|
222
234
|
/** 下载单篇 PDF 到用户目录。 */
|
|
223
235
|
function downloadPaperPdf(taskId, paperId, signal) {
|
|
224
236
|
return request(taskPath(taskId, `/papers/${encodeURIComponent(paperId)}/download`), {
|
|
@@ -226,16 +238,23 @@ function downloadPaperPdf(taskId, paperId, signal) {
|
|
|
226
238
|
signal
|
|
227
239
|
});
|
|
228
240
|
}
|
|
229
|
-
function syncZotero(taskId, paperIds) {
|
|
230
|
-
return request(taskPath(taskId, "/zotero"), writeInit("POST", {
|
|
241
|
+
function syncZotero(taskId, paperIds, target) {
|
|
242
|
+
return request(taskPath(taskId, "/zotero"), writeInit("POST", {
|
|
243
|
+
paperIds,
|
|
244
|
+
...target === void 0 ? {} : {
|
|
245
|
+
target: target.id,
|
|
246
|
+
...target.name === void 0 ? {} : { targetName: target.name },
|
|
247
|
+
...target.filesEditable === void 0 ? {} : { targetFilesEditable: target.filesEditable }
|
|
248
|
+
}
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
/** 点击「查看已下载」前确认本地文件仍在(页面加载后可能被删除)。 */
|
|
252
|
+
function recheckPaperPdf(taskId, paperId) {
|
|
253
|
+
return request(taskPath(taskId, `/papers/${encodeURIComponent(paperId)}/recheck`), writeInit("POST", {}));
|
|
231
254
|
}
|
|
232
|
-
/**
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
const sessionId = currentSessionId();
|
|
236
|
-
if (sessionId !== "") params.set("session", sessionId);
|
|
237
|
-
const suffix = params.toString();
|
|
238
|
-
return `${BASE}/tasks/${encodeURIComponent(taskId)}/papers/${encodeURIComponent(paperId)}/pdf${suffix === "" ? "" : `?${suffix}`}`;
|
|
255
|
+
/** 在系统文件管理器中定位已下载的 PDF(打开所在文件夹并选中文件)。 */
|
|
256
|
+
function revealLocalFile(path) {
|
|
257
|
+
return request("/files/reveal", writeInit("POST", { path }));
|
|
239
258
|
}
|
|
240
259
|
/** Europe PMC 全文 HTML 地址(用于 iframe)。 */
|
|
241
260
|
function paperFullTextUrl(taskId, paperId) {
|
|
@@ -254,6 +273,97 @@ function exportUrl(id, format, query = {}, ids) {
|
|
|
254
273
|
const suffix = params.toString();
|
|
255
274
|
return `${BASE}/export${suffix === "" ? "" : `?${suffix}`}`;
|
|
256
275
|
}
|
|
276
|
+
// src/client/downloadCommandStore.ts
|
|
277
|
+
/**
|
|
278
|
+
* 模型下载指令的客户端暂存与分发。
|
|
279
|
+
*
|
|
280
|
+
* 指令由全局 `/events` 轮询写入,但真正执行下载的是某个任务 tab 里的
|
|
281
|
+
* `TaskPane`;tab 可能还没挂载(focus 正在拉起它),所以这里做模块级
|
|
282
|
+
* 待消费队列,并按 commandId 幂等去重 —— 刷新页面也不会重放旧指令。
|
|
283
|
+
*/
|
|
284
|
+
const SEEN_KEY$1 = "dsh-litsearch.downloads.seen";
|
|
285
|
+
const SEEN_MAX$1 = 200;
|
|
286
|
+
/** 指令是否仍在有效期内(允许客户端时钟略慢 1 分钟)。 */
|
|
287
|
+
function isFreshDownload(command, now = Date.now()) {
|
|
288
|
+
return Number.isFinite(command.at) && command.at <= now + 6e4 && now - command.at <= 6e5;
|
|
289
|
+
}
|
|
290
|
+
function loadSeen$1() {
|
|
291
|
+
try {
|
|
292
|
+
const raw = globalThis.localStorage?.getItem(SEEN_KEY$1);
|
|
293
|
+
if (raw === null || raw === void 0 || raw === "") return [];
|
|
294
|
+
const parsed = JSON.parse(raw);
|
|
295
|
+
if (!Array.isArray(parsed)) return [];
|
|
296
|
+
return parsed.filter((item) => typeof item === "string").slice(-200);
|
|
297
|
+
} catch {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function rememberSeen$1(commandId) {
|
|
302
|
+
seen$1.add(commandId);
|
|
303
|
+
while (seen$1.size > SEEN_MAX$1) {
|
|
304
|
+
const oldest = seen$1.values().next().value;
|
|
305
|
+
if (oldest === void 0) break;
|
|
306
|
+
seen$1.delete(oldest);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
globalThis.localStorage?.setItem(SEEN_KEY$1, JSON.stringify([...seen$1]));
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
const seen$1 = new Set(loadSeen$1());
|
|
313
|
+
let pending$2 = [];
|
|
314
|
+
let version$2 = 0;
|
|
315
|
+
const listeners$3 = /* @__PURE__ */ new Set();
|
|
316
|
+
function subscribe$2(listener) {
|
|
317
|
+
listeners$3.add(listener);
|
|
318
|
+
return () => {
|
|
319
|
+
listeners$3.delete(listener);
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function getVersion$1() {
|
|
323
|
+
return version$2;
|
|
324
|
+
}
|
|
325
|
+
function getServerVersion$1() {
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
function bump$1() {
|
|
329
|
+
version$2 += 1;
|
|
330
|
+
for (const listener of listeners$3) listener();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 把 `/events` 增量里的下载指令写入待消费队列。
|
|
334
|
+
*
|
|
335
|
+
* 不受自动打开开关与首次轮询影响:下载指令总是入队,tab 由工具同时发出的
|
|
336
|
+
* focus 请求拉起,未挂载时指令在此等待。
|
|
337
|
+
*/
|
|
338
|
+
function publishDownloadCommands(commands, screenSessionId) {
|
|
339
|
+
if (commands === void 0 || commands.length === 0) return;
|
|
340
|
+
let added = 0;
|
|
341
|
+
for (const command of commands) {
|
|
342
|
+
if (!isFreshDownload(command)) continue;
|
|
343
|
+
if (seen$1.has(command.commandId)) continue;
|
|
344
|
+
if (command.sessionId !== void 0 && screenSessionId !== void 0 && command.sessionId !== screenSessionId) continue;
|
|
345
|
+
if (pending$2.some((item) => item.commandId === command.commandId)) continue;
|
|
346
|
+
rememberSeen$1(command.commandId);
|
|
347
|
+
pending$2 = [...pending$2, command];
|
|
348
|
+
added += 1;
|
|
349
|
+
}
|
|
350
|
+
if (added > 0) bump$1();
|
|
351
|
+
}
|
|
352
|
+
/** 订阅某个任务的待消费指令;返回数组按到达顺序排列。 */
|
|
353
|
+
function usePendingDownloadCommands(taskId) {
|
|
354
|
+
const current = (0, react.useSyncExternalStore)(subscribe$2, getVersion$1, getServerVersion$1);
|
|
355
|
+
return (0, react.useMemo)(() => {
|
|
356
|
+
if (taskId === null) return [];
|
|
357
|
+
return pending$2.filter((command) => command.taskId === taskId);
|
|
358
|
+
}, [current, taskId]);
|
|
359
|
+
}
|
|
360
|
+
/** 指令已被某个 TaskPane 接走,避免别的 tab 或后续 effect 重复执行。 */
|
|
361
|
+
function markConsumed(commandId) {
|
|
362
|
+
const next = pending$2.filter((command) => command.commandId !== commandId);
|
|
363
|
+
if (next.length === pending$2.length) return;
|
|
364
|
+
pending$2 = next;
|
|
365
|
+
bump$1();
|
|
366
|
+
}
|
|
257
367
|
// src/client/health.ts
|
|
258
368
|
/**
|
|
259
369
|
* `/health` 的共享快照。
|
|
@@ -262,17 +372,17 @@ function exportUrl(id, format, query = {}, ids) {
|
|
|
262
372
|
* Host 侧设置改了不会主动推送,这里用一份模块级快照 + 定时刷新兜底。
|
|
263
373
|
*/
|
|
264
374
|
let snapshot = null;
|
|
265
|
-
const listeners$
|
|
375
|
+
const listeners$2 = /* @__PURE__ */ new Set();
|
|
266
376
|
function emit() {
|
|
267
|
-
for (const listener of listeners$
|
|
377
|
+
for (const listener of listeners$2) listener();
|
|
268
378
|
}
|
|
269
379
|
function getHealth() {
|
|
270
380
|
return snapshot;
|
|
271
381
|
}
|
|
272
382
|
function subscribeHealth(listener) {
|
|
273
|
-
listeners$
|
|
383
|
+
listeners$2.add(listener);
|
|
274
384
|
return () => {
|
|
275
|
-
listeners$
|
|
385
|
+
listeners$2.delete(listener);
|
|
276
386
|
};
|
|
277
387
|
}
|
|
278
388
|
/** 拉取一次健康信息;失败时保留旧快照并返回 null。 */
|
|
@@ -296,28 +406,28 @@ function useHealth() {
|
|
|
296
406
|
* 同一个 client bundle 里主页历史、任务列表弹窗可能挂在不同 tab 上;
|
|
297
407
|
* 任一处新建 / 重命名 / 删除任务后 bump 一个版本号,其它处据此重拉列表。
|
|
298
408
|
*/
|
|
299
|
-
let version = 0;
|
|
300
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
409
|
+
let version$1 = 0;
|
|
410
|
+
const listeners$1 = /* @__PURE__ */ new Set();
|
|
301
411
|
const summaries = /* @__PURE__ */ new Map();
|
|
302
412
|
const summaryListeners = /* @__PURE__ */ new Set();
|
|
303
413
|
function notifyTaskListChanged() {
|
|
304
|
-
version += 1;
|
|
305
|
-
for (const listener of listeners) listener();
|
|
414
|
+
version$1 += 1;
|
|
415
|
+
for (const listener of listeners$1) listener();
|
|
306
416
|
}
|
|
307
|
-
function subscribe(listener) {
|
|
308
|
-
listeners.add(listener);
|
|
417
|
+
function subscribe$1(listener) {
|
|
418
|
+
listeners$1.add(listener);
|
|
309
419
|
return () => {
|
|
310
|
-
listeners.delete(listener);
|
|
420
|
+
listeners$1.delete(listener);
|
|
311
421
|
};
|
|
312
422
|
}
|
|
313
423
|
function getSnapshot() {
|
|
314
|
-
return version;
|
|
424
|
+
return version$1;
|
|
315
425
|
}
|
|
316
426
|
function getServerSnapshot() {
|
|
317
427
|
return 0;
|
|
318
428
|
}
|
|
319
429
|
function useTaskListVersion() {
|
|
320
|
-
return (0, react.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot);
|
|
430
|
+
return (0, react.useSyncExternalStore)(subscribe$1, getSnapshot, getServerSnapshot);
|
|
321
431
|
}
|
|
322
432
|
/**
|
|
323
433
|
* 把 `/events` 拿到的任务摘要广播给已挂载的任务 tab。
|
|
@@ -338,6 +448,93 @@ function subscribeTaskSummary(listener) {
|
|
|
338
448
|
summaryListeners.delete(listener);
|
|
339
449
|
};
|
|
340
450
|
}
|
|
451
|
+
// src/client/zoteroCommandStore.ts
|
|
452
|
+
/**
|
|
453
|
+
* 模型「批量存入 Zotero」指令的客户端暂存与分发。
|
|
454
|
+
*
|
|
455
|
+
* 与 downloadCommandStore 同构但完全独立:两条链路各自幂等、各自排队,
|
|
456
|
+
* 下载指令的改动不会波及 Zotero,反之亦然。指令由全局 `/events` 轮询写入,
|
|
457
|
+
* 真正执行的是任务 tab 里的 `TaskPane`;tab 可能还没挂载(focus 正在拉起它),
|
|
458
|
+
* 所以这里做模块级待消费队列,并按 commandId 去重 —— 刷新页面也不会重放。
|
|
459
|
+
*/
|
|
460
|
+
const SEEN_KEY = "dsh-litsearch.zotero.seen";
|
|
461
|
+
const SEEN_MAX = 200;
|
|
462
|
+
/** 指令是否仍在有效期内(允许客户端时钟略慢 1 分钟)。 */
|
|
463
|
+
function isFreshZotero(command, now = Date.now()) {
|
|
464
|
+
return Number.isFinite(command.at) && command.at <= now + 6e4 && now - command.at <= 6e5;
|
|
465
|
+
}
|
|
466
|
+
function loadSeen() {
|
|
467
|
+
try {
|
|
468
|
+
const raw = globalThis.localStorage?.getItem(SEEN_KEY);
|
|
469
|
+
if (raw === null || raw === void 0 || raw === "") return [];
|
|
470
|
+
const parsed = JSON.parse(raw);
|
|
471
|
+
if (!Array.isArray(parsed)) return [];
|
|
472
|
+
return parsed.filter((item) => typeof item === "string").slice(-200);
|
|
473
|
+
} catch {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function rememberSeen(commandId) {
|
|
478
|
+
seen.add(commandId);
|
|
479
|
+
while (seen.size > SEEN_MAX) {
|
|
480
|
+
const oldest = seen.values().next().value;
|
|
481
|
+
if (oldest === void 0) break;
|
|
482
|
+
seen.delete(oldest);
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
globalThis.localStorage?.setItem(SEEN_KEY, JSON.stringify([...seen]));
|
|
486
|
+
} catch {}
|
|
487
|
+
}
|
|
488
|
+
const seen = new Set(loadSeen());
|
|
489
|
+
let pending$1 = [];
|
|
490
|
+
let version = 0;
|
|
491
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
492
|
+
function subscribe(listener) {
|
|
493
|
+
listeners.add(listener);
|
|
494
|
+
return () => {
|
|
495
|
+
listeners.delete(listener);
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function getVersion() {
|
|
499
|
+
return version;
|
|
500
|
+
}
|
|
501
|
+
function getServerVersion() {
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
function bump() {
|
|
505
|
+
version += 1;
|
|
506
|
+
for (const listener of listeners) listener();
|
|
507
|
+
}
|
|
508
|
+
/** 把 `/events` 增量里的 Zotero 指令写入待消费队列。 */
|
|
509
|
+
function publishZoteroCommands(commands, screenSessionId) {
|
|
510
|
+
if (commands === void 0 || commands.length === 0) return;
|
|
511
|
+
let added = 0;
|
|
512
|
+
for (const command of commands) {
|
|
513
|
+
if (!isFreshZotero(command)) continue;
|
|
514
|
+
if (seen.has(command.commandId)) continue;
|
|
515
|
+
if (command.sessionId !== void 0 && screenSessionId !== void 0 && command.sessionId !== screenSessionId) continue;
|
|
516
|
+
if (pending$1.some((item) => item.commandId === command.commandId)) continue;
|
|
517
|
+
rememberSeen(command.commandId);
|
|
518
|
+
pending$1 = [...pending$1, command];
|
|
519
|
+
added += 1;
|
|
520
|
+
}
|
|
521
|
+
if (added > 0) bump();
|
|
522
|
+
}
|
|
523
|
+
/** 订阅某个任务的待消费指令;返回数组按到达顺序排列。 */
|
|
524
|
+
function usePendingZoteroCommands(taskId) {
|
|
525
|
+
const current = (0, react.useSyncExternalStore)(subscribe, getVersion, getServerVersion);
|
|
526
|
+
return (0, react.useMemo)(() => {
|
|
527
|
+
if (taskId === null) return [];
|
|
528
|
+
return pending$1.filter((command) => command.taskId === taskId);
|
|
529
|
+
}, [current, taskId]);
|
|
530
|
+
}
|
|
531
|
+
/** 指令已被某个 TaskPane 接走,避免别的 tab 或后续 effect 重复执行。 */
|
|
532
|
+
function markZoteroConsumed(commandId) {
|
|
533
|
+
const next = pending$1.filter((command) => command.commandId !== commandId);
|
|
534
|
+
if (next.length === pending$1.length) return;
|
|
535
|
+
pending$1 = next;
|
|
536
|
+
bump();
|
|
537
|
+
}
|
|
341
538
|
// src/client/autoOpen.ts
|
|
342
539
|
/**
|
|
343
540
|
* 模型任务的自动打开 + 任务标题缓存 + 「待打开」队列。
|
|
@@ -555,6 +752,8 @@ function startAutoOpen(options) {
|
|
|
555
752
|
seeded = true;
|
|
556
753
|
rev = page.rev;
|
|
557
754
|
publishTaskSummaries(page.tasks);
|
|
755
|
+
publishDownloadCommands(page.downloads, options.sessionId?.());
|
|
756
|
+
publishZoteroCommands(page.zotero, options.sessionId?.());
|
|
558
757
|
const settings = getHealth()?.settings;
|
|
559
758
|
const autoOpen = settings?.autoOpenTaskTab ?? true;
|
|
560
759
|
if (autoOpen && page.focus !== void 0 && isFreshFocus(page.focus)) {
|
|
@@ -941,18 +1140,32 @@ const dicts = {
|
|
|
941
1140
|
"pdf.status.unknown": "未获取全文",
|
|
942
1141
|
"pdf.status.available": "有全文直链",
|
|
943
1142
|
"pdf.status.downloaded": "已下载",
|
|
1143
|
+
"pdf.status.missing": "文件已删除",
|
|
944
1144
|
"pdf.status.html-only": "仅 HTML 全文",
|
|
945
1145
|
"pdf.status.paywalled": "需订阅",
|
|
946
1146
|
"pdf.status.blocked": "被出版商拒绝",
|
|
947
1147
|
"pdf.status.failed": "获取失败",
|
|
948
|
-
"pdf.
|
|
1148
|
+
"pdf.htmlLoading": "HTML 全文加载中…",
|
|
949
1149
|
"pdf.closePreview": "收起",
|
|
950
|
-
"pdf.download": "下载
|
|
1150
|
+
"pdf.download": "下载",
|
|
1151
|
+
"pdf.downloading": "下载中…",
|
|
1152
|
+
"pdf.downloadedAction": "查看已下载",
|
|
1153
|
+
"pdf.missingAction": "下载文件已删除点击重新下载",
|
|
1154
|
+
"pdf.retry": "重试下载",
|
|
1155
|
+
"pdf.downloadHint": "下载 PDF 并保存到:{path}",
|
|
1156
|
+
"pdf.downloadedHint": "已保存到:{path}\n点击在右侧面板打开该 PDF",
|
|
1157
|
+
"pdf.missingHint": "本地 PDF 已被删除或移走,点击重新下载",
|
|
1158
|
+
"pdf.revealInFolder": "在文件系统中打开",
|
|
1159
|
+
"pdf.downloadingHint": "正在下载 PDF,请稍候…",
|
|
1160
|
+
"pdf.retryHint": "上次下载失败:{message}\n点击重试",
|
|
1161
|
+
"pdf.downloadDirUnknown": "工作目录下的 dsh-litsearch 文件夹",
|
|
951
1162
|
"pdf.fulltext": "HTML 全文",
|
|
952
1163
|
"pdf.zotero": "存入 Zotero",
|
|
1164
|
+
"pdf.zoteroSynced": "Zotero 已同步",
|
|
1165
|
+
"pdf.zoteroRetry": "Zotero 同步失败",
|
|
953
1166
|
"pdf.error": "全文获取失败:{message}",
|
|
954
1167
|
"pdf.downloaded": "已保存:{name}",
|
|
955
|
-
"pdf.htmlFallback": "
|
|
1168
|
+
"pdf.htmlFallback": "未找到可下载的 PDF,已切换为 HTML 全文;可点击 PDF 链接在浏览器打开。",
|
|
956
1169
|
"pdf.zoteroDone": "已存入 Zotero:{n} 篇",
|
|
957
1170
|
"pdf.zoteroPartial": "Zotero:成功 {saved} 篇,失败 {failed} 篇;{message}",
|
|
958
1171
|
"pdf.zoteroFailed": "Zotero 同步失败:{message}",
|
|
@@ -961,7 +1174,11 @@ const dicts = {
|
|
|
961
1174
|
"pdf.zoteroFallbackDismiss": "关闭",
|
|
962
1175
|
"bulk.count": "已选 {n} 篇",
|
|
963
1176
|
"bulk.select": "选择这篇文献",
|
|
964
|
-
"bulk.selectPage": "
|
|
1177
|
+
"bulk.selectPage": "本页全选",
|
|
1178
|
+
"bulk.selectPageHint": "把当前页的文献加入已选,其他页的选中不受影响",
|
|
1179
|
+
"bulk.unselectAll": "取消全选",
|
|
1180
|
+
"bulk.unselectAllHint": "清空所有已选文献(含其他页)",
|
|
1181
|
+
"bulk.keepHint": "已选文献跨页保留,翻页不会丢失",
|
|
965
1182
|
"bulk.clear": "清空",
|
|
966
1183
|
"bulk.setScreening": "设置筛读状态",
|
|
967
1184
|
"bulk.setGroup": "归入分组",
|
|
@@ -971,7 +1188,33 @@ const dicts = {
|
|
|
971
1188
|
"bulk.insert": "加入对话",
|
|
972
1189
|
"bulk.inserted": "已作为引用插入 {n} 篇(编号列表)",
|
|
973
1190
|
"bulk.export": "按选中导出",
|
|
974
|
-
"bulk.downloadDone": "批量下载:成功 {ok} 篇,失败 {failed} 篇",
|
|
1191
|
+
"bulk.downloadDone": "批量下载:成功 {ok} 篇,需手动获取 {manual} 篇,失败 {failed} 篇",
|
|
1192
|
+
"bulk.revealDir": "打开下载目录",
|
|
1193
|
+
"batch.title": "批量下载",
|
|
1194
|
+
"batch.desc": "共 {n} 篇;已下载的文献会自动跳过,状态实时更新。",
|
|
1195
|
+
"batch.total": "共 {n} 篇",
|
|
1196
|
+
"batch.downloaded": "已下载 {n}",
|
|
1197
|
+
"batch.downloading": "下载中 {n}",
|
|
1198
|
+
"batch.pending": "待下载 {n}",
|
|
1199
|
+
"batch.manual": "需手动获取 {n}",
|
|
1200
|
+
"batch.failed": "失败 {n}",
|
|
1201
|
+
"batch.progress": "进度 {done}/{total}",
|
|
1202
|
+
"batch.complete": "下载任务完成",
|
|
1203
|
+
"batch.untitled": "(未获取标题)",
|
|
1204
|
+
"batch.close": "关闭",
|
|
1205
|
+
"batch.modelTitle": "模型发起的批量下载",
|
|
1206
|
+
"batch.modelDesc": "模型已选出 {n} 篇文献,正在逐篇下载;可关闭弹窗或点「取消下载」中止。",
|
|
1207
|
+
"batch.originModel": "模型发起",
|
|
1208
|
+
"batch.source": "来源 {source}",
|
|
1209
|
+
"batch.allSkipped": "清单中的文献均已下载,已跳过",
|
|
1210
|
+
"batch.cancel": "取消下载",
|
|
1211
|
+
"batch.cancelled": "已取消,剩余文献未下载",
|
|
1212
|
+
"batch.empty": "模型指令里没有可下载的文献",
|
|
1213
|
+
"batch.status.pending": "未下载",
|
|
1214
|
+
"batch.status.downloading": "下载中…",
|
|
1215
|
+
"batch.status.downloaded": "已下载",
|
|
1216
|
+
"batch.status.manual": "需手动获取",
|
|
1217
|
+
"batch.status.failed": "失败",
|
|
975
1218
|
"fulltext.loading": "统计全文覆盖…",
|
|
976
1219
|
"fulltext.hint": "统计基于全部 {total} 篇,与概览同口径,不受当前筛选影响;点状态可切换清单,列表分页加载。",
|
|
977
1220
|
"fulltext.hintFallback": "统计暂不可用,仍可按状态查看文献清单。",
|
|
@@ -987,15 +1230,6 @@ const dicts = {
|
|
|
987
1230
|
"annotate.noGroup": "未分组",
|
|
988
1231
|
"annotate.createGroup": "+ 新建分组…",
|
|
989
1232
|
"annotate.groupHint": "还没有分组;可在任务的「分组」页新建,选「未分组」可清除分组",
|
|
990
|
-
"annotate.expand": "标注",
|
|
991
|
-
"annotate.collapse": "收起标注",
|
|
992
|
-
"annotate.note": "备注",
|
|
993
|
-
"annotate.notePlaceholder": "为什么读 / 方法要点 / 可否引用",
|
|
994
|
-
"annotate.tags": "标签",
|
|
995
|
-
"annotate.tagsPlaceholder": "逗号分隔",
|
|
996
|
-
"annotate.shortlistOrder": "必读顺序",
|
|
997
|
-
"annotate.save": "保存标注",
|
|
998
|
-
"annotate.shortlistBadge": "必读 #{n}",
|
|
999
1233
|
"entity.title": "实体",
|
|
1000
1234
|
"entity.count": "{n} 个实体",
|
|
1001
1235
|
"entity.hint": "来自 PubTator3,按标准标识符去重",
|
|
@@ -1036,7 +1270,7 @@ const dicts = {
|
|
|
1036
1270
|
"groups.empty": "还没有分组。先建一个「为什么读」的类别,再逐篇归入。",
|
|
1037
1271
|
"groups.namePlaceholder": "如:核心方法",
|
|
1038
1272
|
"groups.descPlaceholder": "如:用于方法对比",
|
|
1039
|
-
"groups.deleteConfirm": "删除分组「{name}
|
|
1273
|
+
"groups.deleteConfirm": "删除分组「{name}」?组内文献会回到未分组,筛读状态不受影响。",
|
|
1040
1274
|
"groups.saved": "分组已保存",
|
|
1041
1275
|
"groups.saving": "正在保存…",
|
|
1042
1276
|
"groups.retry": "重试保存",
|
|
@@ -1044,11 +1278,6 @@ const dicts = {
|
|
|
1044
1278
|
"notes.taskPlaceholder": "记录检索策略、纳入排除标准、阶段性结论…",
|
|
1045
1279
|
"notes.save": "保存笔记",
|
|
1046
1280
|
"notes.saved": "笔记已保存",
|
|
1047
|
-
"notes.paperSection": "逐篇备注与标签",
|
|
1048
|
-
"notes.empty": "还没有逐篇备注。可在结果卡片的「标注」里写。",
|
|
1049
|
-
"notes.loading": "读取逐篇备注…",
|
|
1050
|
-
"notes.hint": "逐篇备注按当前任务已加载的前 200 篇统计。",
|
|
1051
|
-
"notes.tags": "标签:{value}",
|
|
1052
1281
|
"compare.title": "与旧任务对比",
|
|
1053
1282
|
"compare.base": "基准任务",
|
|
1054
1283
|
"compare.pick": "选择一个旧任务",
|
|
@@ -1171,7 +1400,7 @@ const dicts = {
|
|
|
1171
1400
|
"result.infoLabel": "状态与来源",
|
|
1172
1401
|
"result.infoHint": "筛读状态、命中的数据库、被引次数、开放获取与全文状态",
|
|
1173
1402
|
"result.actionLabel": "快捷操作",
|
|
1174
|
-
"result.actionHint": "
|
|
1403
|
+
"result.actionHint": "对这篇文献执行操作:加入对话、打开详情、下载 PDF 或 HTML 全文、存入 Zotero",
|
|
1175
1404
|
"error.noEditor": "当前面板不在会话上下文中,无法插入到输入框",
|
|
1176
1405
|
"error.draftChanged": "输入框草稿已变化,未插入;请避免在插入期间编辑草稿",
|
|
1177
1406
|
"error.sourceFailed": "数据源 {source} 检索失败:{message}",
|
|
@@ -1192,7 +1421,7 @@ const dicts = {
|
|
|
1192
1421
|
"group.zotero": "Zotero",
|
|
1193
1422
|
"group.zoteroHint": "通过 Zotero 桌面端 connector 保存条目与 PDF;只连接本机 127.0.0.1。",
|
|
1194
1423
|
"field.contactEmail": "联系邮箱",
|
|
1195
|
-
"field.contactEmailHint": "作为 OpenAlex / Crossref / NCBI 的 polite pool
|
|
1424
|
+
"field.contactEmailHint": "作为 OpenAlex / Crossref / NCBI 的 polite pool 标识;填写后还会启用 Unpaywall,按 DOI 补全开放获取 PDF 地址,提升批量下载成功率",
|
|
1196
1425
|
"field.ncbiApiKey": "NCBI API Key",
|
|
1197
1426
|
"field.ncbiApiKeyHint": "可选;配置后 PubMed E-utilities 额度从 3 次/秒提升到 10 次/秒",
|
|
1198
1427
|
"field.s2ApiKey": "Semantic Scholar Key",
|
|
@@ -1279,6 +1508,45 @@ const dicts = {
|
|
|
1279
1508
|
"zotero.probeOk": "连接正常(Zotero {version});当前分类:{name}",
|
|
1280
1509
|
"zotero.probeOkNoCollection": "连接正常,但 Zotero 里还没有选中分类",
|
|
1281
1510
|
"zotero.probeFail": "连接失败:{message}",
|
|
1511
|
+
"zotero.dialog.title": "存入 Zotero",
|
|
1512
|
+
"zotero.dialog.desc": "共 {n} 篇。选择要存入的 Zotero 目录,再点「同步」。",
|
|
1513
|
+
"zotero.dialog.modelTitle": "模型发起的批量存入 Zotero",
|
|
1514
|
+
"zotero.dialog.modelDescAuto": "模型已选出 {n} 篇并指定目录,正在自动同步;可随时取消。",
|
|
1515
|
+
"zotero.dialog.modelDescPick": "模型已选出 {n} 篇,请确认目标目录后点「同步」。",
|
|
1516
|
+
"zotero.dialog.originModel": "模型发起",
|
|
1517
|
+
"zotero.dialog.modelTarget": "模型指定目录:{name}",
|
|
1518
|
+
"zotero.dialog.modelTargetMissing": "模型指定的目录「{name}」已不在 Zotero 目录列表中,请手动选择目录后再同步。",
|
|
1519
|
+
"zotero.dialog.modelNoAuto": "无法读取 Zotero 目录,已取消自动同步;请恢复后手动点「同步」。",
|
|
1520
|
+
"zotero.dialog.modelEmpty": "模型指令里没有可同步的文献",
|
|
1521
|
+
"zotero.dialog.source": "来源 {source}",
|
|
1522
|
+
"zotero.dialog.target": "目标目录",
|
|
1523
|
+
"zotero.dialog.current": "(当前选中)",
|
|
1524
|
+
"zotero.dialog.readonly": "(不可写附件)",
|
|
1525
|
+
"zotero.dialog.loading": "正在读取 Zotero 目录…",
|
|
1526
|
+
"zotero.dialog.loadFail": "无法读取 Zotero 目录:{message}",
|
|
1527
|
+
"zotero.dialog.unknownError": "未知错误",
|
|
1528
|
+
"zotero.dialog.noTarget": "Zotero 里没有可写入的目录,请先在 Zotero 中新建分类",
|
|
1529
|
+
"zotero.dialog.rerunHint": "重新同步会在 Zotero 里新建条目,不会覆盖已有条目。",
|
|
1530
|
+
"zotero.dialog.total": "共 {n} 篇",
|
|
1531
|
+
"zotero.dialog.synced": "已同步 {n}",
|
|
1532
|
+
"zotero.dialog.syncingCount": "同步中 {n}",
|
|
1533
|
+
"zotero.dialog.pending": "待同步 {n}",
|
|
1534
|
+
"zotero.dialog.failed": "失败 {n}",
|
|
1535
|
+
"zotero.dialog.running": "正在同步中…",
|
|
1536
|
+
"zotero.dialog.done": "同步完成:{n} 篇已存入 Zotero",
|
|
1537
|
+
"zotero.dialog.partial": "同步结束:成功 {saved} 篇,失败 {failed} 篇",
|
|
1538
|
+
"zotero.dialog.progress": "已处理 {done}/{total}",
|
|
1539
|
+
"zotero.dialog.sync": "同步",
|
|
1540
|
+
"zotero.dialog.syncing": "同步中…",
|
|
1541
|
+
"zotero.dialog.resync": "重新同步",
|
|
1542
|
+
"zotero.dialog.cancel": "取消",
|
|
1543
|
+
"zotero.dialog.close": "关闭",
|
|
1544
|
+
"zotero.dialog.untitled": "(无标题)",
|
|
1545
|
+
"zotero.dialog.failFallback": "同步失败",
|
|
1546
|
+
"zotero.dialog.status.pending": "待同步",
|
|
1547
|
+
"zotero.dialog.status.syncing": "同步中…",
|
|
1548
|
+
"zotero.dialog.status.synced": "已同步",
|
|
1549
|
+
"zotero.dialog.status.failed": "失败",
|
|
1282
1550
|
"form.unavailable": "该插件当前未加载,暂时无法配置。",
|
|
1283
1551
|
"form.readOnly": "本部署的设置为只读。",
|
|
1284
1552
|
"form.saveFailed": "本部署没有接受这些值,已保留供你修改。",
|
|
@@ -1472,18 +1740,32 @@ const dicts = {
|
|
|
1472
1740
|
"pdf.status.unknown": "No full text yet",
|
|
1473
1741
|
"pdf.status.available": "Full-text link",
|
|
1474
1742
|
"pdf.status.downloaded": "Downloaded",
|
|
1743
|
+
"pdf.status.missing": "File deleted",
|
|
1475
1744
|
"pdf.status.html-only": "HTML full text",
|
|
1476
1745
|
"pdf.status.paywalled": "Paywalled",
|
|
1477
1746
|
"pdf.status.blocked": "Blocked by publisher",
|
|
1478
1747
|
"pdf.status.failed": "Fetch failed",
|
|
1479
|
-
"pdf.
|
|
1748
|
+
"pdf.htmlLoading": "Loading full text…",
|
|
1480
1749
|
"pdf.closePreview": "Close",
|
|
1481
|
-
"pdf.download": "Download
|
|
1750
|
+
"pdf.download": "Download",
|
|
1751
|
+
"pdf.downloading": "Downloading…",
|
|
1752
|
+
"pdf.downloadedAction": "View downloaded",
|
|
1753
|
+
"pdf.missingAction": "Downloaded file deleted — click to re-download",
|
|
1754
|
+
"pdf.retry": "Retry download",
|
|
1755
|
+
"pdf.downloadHint": "Download the PDF to: {path}",
|
|
1756
|
+
"pdf.downloadedHint": "Saved to: {path}\nClick to open the PDF in the right panel",
|
|
1757
|
+
"pdf.missingHint": "The local PDF was deleted or moved. Click to download again.",
|
|
1758
|
+
"pdf.revealInFolder": "Show in file manager",
|
|
1759
|
+
"pdf.downloadingHint": "Downloading PDF, please wait…",
|
|
1760
|
+
"pdf.retryHint": "Last download failed: {message}\nClick to retry",
|
|
1761
|
+
"pdf.downloadDirUnknown": "the dsh-litsearch folder under the workspace",
|
|
1482
1762
|
"pdf.fulltext": "HTML full text",
|
|
1483
1763
|
"pdf.zotero": "Save to Zotero",
|
|
1764
|
+
"pdf.zoteroSynced": "Synced to Zotero",
|
|
1765
|
+
"pdf.zoteroRetry": "Zotero sync failed",
|
|
1484
1766
|
"pdf.error": "Full text failed: {message}",
|
|
1485
1767
|
"pdf.downloaded": "Saved: {name}",
|
|
1486
|
-
"pdf.htmlFallback": "No downloadable PDF;
|
|
1768
|
+
"pdf.htmlFallback": "No downloadable PDF found; switched to HTML full text. Open the PDF link in a browser if needed.",
|
|
1487
1769
|
"pdf.zoteroDone": "Saved to Zotero: {n}",
|
|
1488
1770
|
"pdf.zoteroPartial": "Zotero: {saved} saved, {failed} failed; {message}",
|
|
1489
1771
|
"pdf.zoteroFailed": "Zotero sync failed: {message}",
|
|
@@ -1492,7 +1774,11 @@ const dicts = {
|
|
|
1492
1774
|
"pdf.zoteroFallbackDismiss": "Dismiss",
|
|
1493
1775
|
"bulk.count": "{n} selected",
|
|
1494
1776
|
"bulk.select": "Select this paper",
|
|
1495
|
-
"bulk.selectPage": "Select page",
|
|
1777
|
+
"bulk.selectPage": "Select this page",
|
|
1778
|
+
"bulk.selectPageHint": "Add this page to the selection; other pages stay selected",
|
|
1779
|
+
"bulk.unselectAll": "Clear all",
|
|
1780
|
+
"bulk.unselectAllHint": "Clear the whole selection, including other pages",
|
|
1781
|
+
"bulk.keepHint": "Selection is kept when you change pages",
|
|
1496
1782
|
"bulk.clear": "Clear",
|
|
1497
1783
|
"bulk.setScreening": "Set screening",
|
|
1498
1784
|
"bulk.setGroup": "Assign group",
|
|
@@ -1502,7 +1788,33 @@ const dicts = {
|
|
|
1502
1788
|
"bulk.insert": "Add to chat",
|
|
1503
1789
|
"bulk.inserted": "Inserted {n} papers as a quoted numbered list",
|
|
1504
1790
|
"bulk.export": "Export selected",
|
|
1505
|
-
"bulk.downloadDone": "Download: {ok} succeeded, {failed} failed",
|
|
1791
|
+
"bulk.downloadDone": "Download: {ok} succeeded, {manual} need manual save, {failed} failed",
|
|
1792
|
+
"bulk.revealDir": "Open download folder",
|
|
1793
|
+
"batch.title": "Batch download",
|
|
1794
|
+
"batch.desc": "Downloading {n} papers; already downloaded ones are skipped. Status updates live.",
|
|
1795
|
+
"batch.total": "{n} total",
|
|
1796
|
+
"batch.downloaded": "{n} downloaded",
|
|
1797
|
+
"batch.downloading": "{n} downloading",
|
|
1798
|
+
"batch.pending": "{n} pending",
|
|
1799
|
+
"batch.manual": "{n} need manual save",
|
|
1800
|
+
"batch.failed": "{n} failed",
|
|
1801
|
+
"batch.progress": "Progress {done}/{total}",
|
|
1802
|
+
"batch.complete": "Download task complete",
|
|
1803
|
+
"batch.untitled": "(title unavailable)",
|
|
1804
|
+
"batch.close": "Close",
|
|
1805
|
+
"batch.modelTitle": "Model-initiated batch download",
|
|
1806
|
+
"batch.modelDesc": "The model selected {n} papers; downloading them here. You can close this dialog or cancel.",
|
|
1807
|
+
"batch.originModel": "Model request",
|
|
1808
|
+
"batch.source": "Source: {source}",
|
|
1809
|
+
"batch.allSkipped": "All papers in this list were already downloaded and were skipped",
|
|
1810
|
+
"batch.cancel": "Cancel download",
|
|
1811
|
+
"batch.cancelled": "Cancelled; remaining papers were not downloaded",
|
|
1812
|
+
"batch.empty": "No papers to download in the model request",
|
|
1813
|
+
"batch.status.pending": "Not downloaded",
|
|
1814
|
+
"batch.status.downloading": "Downloading…",
|
|
1815
|
+
"batch.status.downloaded": "Downloaded",
|
|
1816
|
+
"batch.status.manual": "Manual save",
|
|
1817
|
+
"batch.status.failed": "Failed",
|
|
1506
1818
|
"fulltext.loading": "Computing full-text coverage…",
|
|
1507
1819
|
"fulltext.hint": "Coverage covers all {total} papers (same basis as the overview) and ignores current filters. Click a status to switch lists; the list loads in pages.",
|
|
1508
1820
|
"fulltext.hintFallback": "Coverage is unavailable; you can still browse papers by status.",
|
|
@@ -1518,15 +1830,6 @@ const dicts = {
|
|
|
1518
1830
|
"annotate.noGroup": "Ungrouped",
|
|
1519
1831
|
"annotate.createGroup": "+ New group…",
|
|
1520
1832
|
"annotate.groupHint": "No groups yet; create one on the Groups tab, or pick Ungrouped to clear",
|
|
1521
|
-
"annotate.expand": "Annotate",
|
|
1522
|
-
"annotate.collapse": "Hide annotation",
|
|
1523
|
-
"annotate.note": "Note",
|
|
1524
|
-
"annotate.notePlaceholder": "Why read it / method / quotable",
|
|
1525
|
-
"annotate.tags": "Tags",
|
|
1526
|
-
"annotate.tagsPlaceholder": "comma separated",
|
|
1527
|
-
"annotate.shortlistOrder": "Shortlist order",
|
|
1528
|
-
"annotate.save": "Save annotation",
|
|
1529
|
-
"annotate.shortlistBadge": "Shortlist #{n}",
|
|
1530
1833
|
"entity.title": "Entities",
|
|
1531
1834
|
"entity.count": "{n} entities",
|
|
1532
1835
|
"entity.hint": "From PubTator3, deduped by standard identifier",
|
|
@@ -1575,11 +1878,6 @@ const dicts = {
|
|
|
1575
1878
|
"notes.taskPlaceholder": "Search strategy, inclusion criteria, interim conclusions…",
|
|
1576
1879
|
"notes.save": "Save notes",
|
|
1577
1880
|
"notes.saved": "Notes saved",
|
|
1578
|
-
"notes.paperSection": "Per-paper notes and tags",
|
|
1579
|
-
"notes.empty": "No per-paper notes yet. Use \"Annotate\" on a result card.",
|
|
1580
|
-
"notes.loading": "Reading per-paper notes…",
|
|
1581
|
-
"notes.hint": "Per-paper notes cover the first 200 loaded papers of this task.",
|
|
1582
|
-
"notes.tags": "Tags: {value}",
|
|
1583
1881
|
"compare.title": "Compare with an older task",
|
|
1584
1882
|
"compare.base": "Base task",
|
|
1585
1883
|
"compare.pick": "Pick an older task",
|
|
@@ -1702,7 +2000,7 @@ const dicts = {
|
|
|
1702
2000
|
"result.infoLabel": "Status & sources",
|
|
1703
2001
|
"result.infoHint": "Screening status, matched databases, citations, open access and full-text status",
|
|
1704
2002
|
"result.actionLabel": "Actions",
|
|
1705
|
-
"result.actionHint": "Actions on this paper: insert into chat, open details,
|
|
2003
|
+
"result.actionHint": "Actions on this paper: insert into chat, open details, download PDF or HTML full text, save to Zotero",
|
|
1706
2004
|
"error.noEditor": "This panel is not in a conversation context; cannot insert into the composer",
|
|
1707
2005
|
"error.draftChanged": "The draft changed; nothing was inserted. Avoid editing while inserting.",
|
|
1708
2006
|
"error.sourceFailed": "Source {source} failed: {message}",
|
|
@@ -1723,7 +2021,7 @@ const dicts = {
|
|
|
1723
2021
|
"group.zotero": "Zotero",
|
|
1724
2022
|
"group.zoteroHint": "Saves items and PDFs through the Zotero desktop connector; only 127.0.0.1 is allowed.",
|
|
1725
2023
|
"field.contactEmail": "Contact email",
|
|
1726
|
-
"field.contactEmailHint": "
|
|
2024
|
+
"field.contactEmailHint": "Polite-pool identifier for OpenAlex / Crossref / NCBI; also enables Unpaywall, which adds open-access PDF links by DOI and improves batch downloads",
|
|
1727
2025
|
"field.ncbiApiKey": "NCBI API key",
|
|
1728
2026
|
"field.ncbiApiKeyHint": "Optional; raises PubMed E-utilities quota from 3 req/s to 10 req/s",
|
|
1729
2027
|
"field.s2ApiKey": "Semantic Scholar key",
|
|
@@ -1810,6 +2108,45 @@ const dicts = {
|
|
|
1810
2108
|
"zotero.probeOk": "Connected (Zotero {version}); selected collection: {name}",
|
|
1811
2109
|
"zotero.probeOkNoCollection": "Connected, but no collection is selected in Zotero",
|
|
1812
2110
|
"zotero.probeFail": "Connection failed: {message}",
|
|
2111
|
+
"zotero.dialog.title": "Save to Zotero",
|
|
2112
|
+
"zotero.dialog.desc": "{n} item(s). Choose a Zotero collection, then click Sync.",
|
|
2113
|
+
"zotero.dialog.modelTitle": "Model-initiated Zotero sync",
|
|
2114
|
+
"zotero.dialog.modelDescAuto": "The model selected {n} papers and a collection; syncing automatically. You can cancel.",
|
|
2115
|
+
"zotero.dialog.modelDescPick": "The model selected {n} papers. Confirm the collection, then click Sync.",
|
|
2116
|
+
"zotero.dialog.originModel": "Model request",
|
|
2117
|
+
"zotero.dialog.modelTarget": "Collection requested by the model: {name}",
|
|
2118
|
+
"zotero.dialog.modelTargetMissing": "The collection \"{name}\" requested by the model is no longer available. Choose one manually before syncing.",
|
|
2119
|
+
"zotero.dialog.modelNoAuto": "Could not load Zotero collections, so auto-sync was cancelled. Click Sync manually once available.",
|
|
2120
|
+
"zotero.dialog.modelEmpty": "No papers to sync in the model request",
|
|
2121
|
+
"zotero.dialog.source": "Source: {source}",
|
|
2122
|
+
"zotero.dialog.target": "Target collection",
|
|
2123
|
+
"zotero.dialog.current": "(selected in Zotero)",
|
|
2124
|
+
"zotero.dialog.readonly": "(attachments not writable)",
|
|
2125
|
+
"zotero.dialog.loading": "Loading Zotero collections…",
|
|
2126
|
+
"zotero.dialog.loadFail": "Could not load Zotero collections: {message}",
|
|
2127
|
+
"zotero.dialog.unknownError": "Unknown error",
|
|
2128
|
+
"zotero.dialog.noTarget": "No writable collection found. Create one in Zotero first.",
|
|
2129
|
+
"zotero.dialog.rerunHint": "Re-syncing creates new items in Zotero; existing items are not overwritten.",
|
|
2130
|
+
"zotero.dialog.total": "Total {n}",
|
|
2131
|
+
"zotero.dialog.synced": "Synced {n}",
|
|
2132
|
+
"zotero.dialog.syncingCount": "Syncing {n}",
|
|
2133
|
+
"zotero.dialog.pending": "Pending {n}",
|
|
2134
|
+
"zotero.dialog.failed": "Failed {n}",
|
|
2135
|
+
"zotero.dialog.running": "Syncing…",
|
|
2136
|
+
"zotero.dialog.done": "Done: {n} item(s) saved to Zotero",
|
|
2137
|
+
"zotero.dialog.partial": "Finished: {saved} saved, {failed} failed",
|
|
2138
|
+
"zotero.dialog.progress": "Processed {done}/{total}",
|
|
2139
|
+
"zotero.dialog.sync": "Sync",
|
|
2140
|
+
"zotero.dialog.syncing": "Syncing…",
|
|
2141
|
+
"zotero.dialog.resync": "Sync again",
|
|
2142
|
+
"zotero.dialog.cancel": "Cancel",
|
|
2143
|
+
"zotero.dialog.close": "Close",
|
|
2144
|
+
"zotero.dialog.untitled": "(untitled)",
|
|
2145
|
+
"zotero.dialog.failFallback": "Sync failed",
|
|
2146
|
+
"zotero.dialog.status.pending": "Pending",
|
|
2147
|
+
"zotero.dialog.status.syncing": "Syncing…",
|
|
2148
|
+
"zotero.dialog.status.synced": "Synced",
|
|
2149
|
+
"zotero.dialog.status.failed": "Failed",
|
|
1813
2150
|
"form.unavailable": "This plugin is not loaded, so it cannot be configured right now.",
|
|
1814
2151
|
"form.readOnly": "This deployment stores settings read-only.",
|
|
1815
2152
|
"form.saveFailed": "This deployment did not accept these values; they were kept for you to fix.",
|
|
@@ -1992,11 +2329,32 @@ const actionRowStyle = {
|
|
|
1992
2329
|
gap: 10,
|
|
1993
2330
|
marginTop: 8
|
|
1994
2331
|
};
|
|
1995
|
-
/**
|
|
2332
|
+
/** 提醒浮层容器:贴面板底部堆叠,不遮挡操作按钮的点击。 */
|
|
2333
|
+
const toastStackStyle = {
|
|
2334
|
+
position: "absolute",
|
|
2335
|
+
left: 15,
|
|
2336
|
+
right: 15,
|
|
2337
|
+
bottom: 14,
|
|
2338
|
+
zIndex: 30,
|
|
2339
|
+
display: "flex",
|
|
2340
|
+
flexDirection: "column",
|
|
2341
|
+
gap: 6,
|
|
2342
|
+
pointerEvents: "none"
|
|
2343
|
+
};
|
|
2344
|
+
/** 底部自动消失的提醒:黑底白字。 */
|
|
1996
2345
|
const toastStyle = {
|
|
2346
|
+
padding: "8px 12px",
|
|
2347
|
+
borderRadius: 8,
|
|
2348
|
+
border: "1px solid rgba(255,255,255,0.16)",
|
|
2349
|
+
background: "rgba(0,0,0,0.92)",
|
|
2350
|
+
color: "#fff",
|
|
1997
2351
|
fontSize: 12,
|
|
1998
|
-
|
|
2352
|
+
lineHeight: 1.5,
|
|
2353
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.28)",
|
|
2354
|
+
pointerEvents: "none"
|
|
1999
2355
|
};
|
|
2356
|
+
/** 下载失败等错误提醒:同样黑底白字,左侧红条区分。 */
|
|
2357
|
+
const toastErrorStyle = { borderLeft: `3px solid ${ERROR}` };
|
|
2000
2358
|
// src/client/TaskListDialog.tsx
|
|
2001
2359
|
/**
|
|
2002
2360
|
* 右上角「搜索任务列表」弹窗:列出全部任务,可打开、重命名、保留、删除。
|
|
@@ -2523,7 +2881,7 @@ function SyntaxHint({ t, sources, texts, infos }) {
|
|
|
2523
2881
|
const names = affected.map((source) => t(`source.${source}`)).join(" / ");
|
|
2524
2882
|
const title = (infos ?? []).filter((info) => affected.includes(info.id)).map((info) => `${info.label}:${info.syntax}`).join("\n");
|
|
2525
2883
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2526
|
-
style: HINT_STYLE$
|
|
2884
|
+
style: HINT_STYLE$4,
|
|
2527
2885
|
title: title === "" ? void 0 : title,
|
|
2528
2886
|
children: tpl(t("search.syntaxWarn"), { sources: names })
|
|
2529
2887
|
});
|
|
@@ -2533,7 +2891,7 @@ function sourceSyntaxTitle(infos, id) {
|
|
|
2533
2891
|
const info = infos?.find((entry) => entry.id === id);
|
|
2534
2892
|
return info === void 0 ? void 0 : info.syntax;
|
|
2535
2893
|
}
|
|
2536
|
-
const HINT_STYLE$
|
|
2894
|
+
const HINT_STYLE$4 = {
|
|
2537
2895
|
fontSize: 11,
|
|
2538
2896
|
lineHeight: 1.5,
|
|
2539
2897
|
color: "#b26a00"
|
|
@@ -2655,7 +3013,10 @@ function LitsearchPage({ t, onOpenTask }) {
|
|
|
2655
3013
|
};
|
|
2656
3014
|
const pageSize = health?.settings.defaultPageSize ?? 20;
|
|
2657
3015
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2658
|
-
style:
|
|
3016
|
+
style: {
|
|
3017
|
+
...panelStyle,
|
|
3018
|
+
position: "relative"
|
|
3019
|
+
},
|
|
2659
3020
|
children: [
|
|
2660
3021
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2661
3022
|
style: pageHeaderStyle,
|
|
@@ -2747,15 +3108,20 @@ function LitsearchPage({ t, onOpenTask }) {
|
|
|
2747
3108
|
},
|
|
2748
3109
|
children: error
|
|
2749
3110
|
}),
|
|
2750
|
-
pending.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
2751
|
-
style:
|
|
2752
|
-
children:
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3111
|
+
pending.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3112
|
+
style: toastStackStyle,
|
|
3113
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3114
|
+
style: bannerStyle,
|
|
3115
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3116
|
+
style: { minWidth: 0 },
|
|
3117
|
+
children: tpl(t("pending.title"), { n: pending.length })
|
|
3118
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3119
|
+
type: "button",
|
|
3120
|
+
style: bannerButtonStyle,
|
|
3121
|
+
onClick: openPending,
|
|
3122
|
+
children: t("pending.open")
|
|
3123
|
+
})]
|
|
3124
|
+
})
|
|
2759
3125
|
}),
|
|
2760
3126
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
2761
3127
|
style: sectionTitleStyle,
|
|
@@ -2884,14 +3250,18 @@ const historyMetaStyle = {
|
|
|
2884
3250
|
opacity: .66
|
|
2885
3251
|
};
|
|
2886
3252
|
const bannerStyle = {
|
|
3253
|
+
...toastStyle,
|
|
3254
|
+
pointerEvents: "auto",
|
|
2887
3255
|
display: "flex",
|
|
2888
3256
|
alignItems: "center",
|
|
2889
3257
|
justifyContent: "space-between",
|
|
2890
|
-
gap: 8
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3258
|
+
gap: 8
|
|
3259
|
+
};
|
|
3260
|
+
const bannerButtonStyle = {
|
|
3261
|
+
...linkButtonStyle,
|
|
3262
|
+
color: "#fff",
|
|
3263
|
+
textDecoration: "underline",
|
|
3264
|
+
flexShrink: 0
|
|
2895
3265
|
};
|
|
2896
3266
|
// src/client/compose.ts
|
|
2897
3267
|
/** 插入时摘要最多保留多少字符,避免草稿过长。 */
|
|
@@ -2986,6 +3356,133 @@ function taskConditionsText(t, detail) {
|
|
|
2986
3356
|
function truncate(value, max) {
|
|
2987
3357
|
return value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
2988
3358
|
}
|
|
3359
|
+
// src/client/DownloadButton.tsx
|
|
3360
|
+
/**
|
|
3361
|
+
* 「下载 PDF / 查看已下载」动作按钮。
|
|
3362
|
+
*
|
|
3363
|
+
* 未下载时是单个下载按钮;已下载时变为「查看已下载 ▾」拆分按钮:点文字在右侧
|
|
3364
|
+
* 预览本地 PDF,点箭头展开菜单,可在系统文件管理器中定位该文件。文件被删除后
|
|
3365
|
+
* 回到普通按钮并提示点击重新下载。
|
|
3366
|
+
*/
|
|
3367
|
+
/** 已下载状态的绿色,与徽章 / 状态色一致。 */
|
|
3368
|
+
const DOWNLOADED_COLOR = "#2e9e5b";
|
|
3369
|
+
/** 本地文件已被删除的警示色。 */
|
|
3370
|
+
const MISSING_COLOR = "#d64545";
|
|
3371
|
+
function DownloadButton({ t, label, hint, downloadedPath, missing = false, downloading = false, disabled = false, style, onDownload, onOpen, onReveal }) {
|
|
3372
|
+
const [menuOpen, setMenuOpen] = (0, react.useState)(false);
|
|
3373
|
+
const groupRef = (0, react.useRef)(null);
|
|
3374
|
+
const path = downloadedPath ?? "";
|
|
3375
|
+
const downloaded = !missing && path !== "" && onOpen !== void 0;
|
|
3376
|
+
const color = missing ? MISSING_COLOR : downloaded ? DOWNLOADED_COLOR : void 0;
|
|
3377
|
+
(0, react.useEffect)(() => {
|
|
3378
|
+
if (!menuOpen) return;
|
|
3379
|
+
const closeOnOutside = (event) => {
|
|
3380
|
+
if (groupRef.current !== null && !groupRef.current.contains(event.target)) setMenuOpen(false);
|
|
3381
|
+
};
|
|
3382
|
+
const closeOnEscape = (event) => {
|
|
3383
|
+
if (event.key === "Escape") setMenuOpen(false);
|
|
3384
|
+
};
|
|
3385
|
+
document.addEventListener("mousedown", closeOnOutside);
|
|
3386
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
3387
|
+
return () => {
|
|
3388
|
+
document.removeEventListener("mousedown", closeOnOutside);
|
|
3389
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
3390
|
+
};
|
|
3391
|
+
}, [menuOpen]);
|
|
3392
|
+
const main = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3393
|
+
type: "button",
|
|
3394
|
+
style: {
|
|
3395
|
+
...style,
|
|
3396
|
+
display: "inline-flex",
|
|
3397
|
+
alignItems: "center",
|
|
3398
|
+
gap: 4,
|
|
3399
|
+
...color === void 0 ? null : { color }
|
|
3400
|
+
},
|
|
3401
|
+
disabled: disabled || downloading,
|
|
3402
|
+
title: hint,
|
|
3403
|
+
"aria-busy": downloading,
|
|
3404
|
+
onClick: () => {
|
|
3405
|
+
if (downloaded) onOpen?.(path);
|
|
3406
|
+
else onDownload();
|
|
3407
|
+
},
|
|
3408
|
+
children: [downloading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpinnerIcon, {}), label]
|
|
3409
|
+
});
|
|
3410
|
+
if (!downloaded || onReveal === void 0) return main;
|
|
3411
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3412
|
+
ref: groupRef,
|
|
3413
|
+
style: GROUP_STYLE$2,
|
|
3414
|
+
children: [
|
|
3415
|
+
main,
|
|
3416
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3417
|
+
type: "button",
|
|
3418
|
+
style: {
|
|
3419
|
+
...style,
|
|
3420
|
+
...ARROW_STYLE,
|
|
3421
|
+
color: DOWNLOADED_COLOR
|
|
3422
|
+
},
|
|
3423
|
+
"aria-haspopup": "menu",
|
|
3424
|
+
"aria-expanded": menuOpen,
|
|
3425
|
+
"aria-label": t("pdf.revealInFolder"),
|
|
3426
|
+
title: t("pdf.revealInFolder"),
|
|
3427
|
+
disabled,
|
|
3428
|
+
onClick: () => setMenuOpen((value) => !value),
|
|
3429
|
+
children: "▾"
|
|
3430
|
+
}),
|
|
3431
|
+
menuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3432
|
+
role: "menu",
|
|
3433
|
+
style: MENU_STYLE,
|
|
3434
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3435
|
+
type: "button",
|
|
3436
|
+
role: "menuitem",
|
|
3437
|
+
style: MENU_ITEM_STYLE,
|
|
3438
|
+
onClick: () => {
|
|
3439
|
+
setMenuOpen(false);
|
|
3440
|
+
onReveal?.(path);
|
|
3441
|
+
},
|
|
3442
|
+
children: t("pdf.revealInFolder")
|
|
3443
|
+
})
|
|
3444
|
+
})
|
|
3445
|
+
]
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
const GROUP_STYLE$2 = {
|
|
3449
|
+
position: "relative",
|
|
3450
|
+
display: "inline-flex",
|
|
3451
|
+
alignItems: "center",
|
|
3452
|
+
gap: 2
|
|
3453
|
+
};
|
|
3454
|
+
const ARROW_STYLE = {
|
|
3455
|
+
padding: "0 1px",
|
|
3456
|
+
lineHeight: 1,
|
|
3457
|
+
fontSize: 20
|
|
3458
|
+
};
|
|
3459
|
+
const MENU_STYLE = {
|
|
3460
|
+
position: "absolute",
|
|
3461
|
+
top: "100%",
|
|
3462
|
+
right: 0,
|
|
3463
|
+
marginTop: 3,
|
|
3464
|
+
zIndex: 40,
|
|
3465
|
+
display: "flex",
|
|
3466
|
+
flexDirection: "column",
|
|
3467
|
+
minWidth: 132,
|
|
3468
|
+
padding: 2,
|
|
3469
|
+
borderRadius: 6,
|
|
3470
|
+
border: "1px solid rgba(127,127,127,0.3)",
|
|
3471
|
+
background: "#fff",
|
|
3472
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.18)"
|
|
3473
|
+
};
|
|
3474
|
+
const MENU_ITEM_STYLE = {
|
|
3475
|
+
padding: "5px 8px",
|
|
3476
|
+
border: "none",
|
|
3477
|
+
borderRadius: 4,
|
|
3478
|
+
background: "transparent",
|
|
3479
|
+
color: "#1f2328",
|
|
3480
|
+
fontSize: 12,
|
|
3481
|
+
lineHeight: 1.4,
|
|
3482
|
+
textAlign: "left",
|
|
3483
|
+
whiteSpace: "nowrap",
|
|
3484
|
+
cursor: "pointer"
|
|
3485
|
+
};
|
|
2989
3486
|
// src/client/Entities.tsx
|
|
2990
3487
|
/**
|
|
2991
3488
|
* PubTator3 实体标注面板。
|
|
@@ -3059,9 +3556,9 @@ function EntityPanel({ t, taskId, paperId, pmid }) {
|
|
|
3059
3556
|
});
|
|
3060
3557
|
const groups = groupByType(entities);
|
|
3061
3558
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3062
|
-
style: WRAP_STYLE$
|
|
3559
|
+
style: WRAP_STYLE$7,
|
|
3063
3560
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3064
|
-
style: HINT_STYLE$
|
|
3561
|
+
style: HINT_STYLE$3,
|
|
3065
3562
|
children: [
|
|
3066
3563
|
tpl(t("entity.count"), { n: entities.length }),
|
|
3067
3564
|
" · ",
|
|
@@ -3104,7 +3601,7 @@ function entityTitle(entity) {
|
|
|
3104
3601
|
entity.identifier
|
|
3105
3602
|
].filter((part) => part !== "").join(" · ");
|
|
3106
3603
|
}
|
|
3107
|
-
const WRAP_STYLE$
|
|
3604
|
+
const WRAP_STYLE$7 = {
|
|
3108
3605
|
display: "flex",
|
|
3109
3606
|
flexDirection: "column",
|
|
3110
3607
|
gap: 5,
|
|
@@ -3113,7 +3610,7 @@ const WRAP_STYLE$6 = {
|
|
|
3113
3610
|
borderRadius: 6,
|
|
3114
3611
|
background: "rgba(127,127,127,0.07)"
|
|
3115
3612
|
};
|
|
3116
|
-
const HINT_STYLE$
|
|
3613
|
+
const HINT_STYLE$3 = {
|
|
3117
3614
|
fontSize: 11,
|
|
3118
3615
|
opacity: .68
|
|
3119
3616
|
};
|
|
@@ -3159,76 +3656,762 @@ function insertQuote(actions, text, summary) {
|
|
|
3159
3656
|
const span = actions.captureInsertion();
|
|
3160
3657
|
return actions.insertText(text, span) ? "inserted" : "draft-changed";
|
|
3161
3658
|
}
|
|
3162
|
-
// src/client/
|
|
3659
|
+
// src/client/PreviewFrame.tsx
|
|
3163
3660
|
/**
|
|
3164
|
-
*
|
|
3661
|
+
* 内联 HTML 全文预览框。
|
|
3165
3662
|
*
|
|
3166
|
-
*
|
|
3167
|
-
* 单篇详情端点,避免为了打开一篇论文拉取整页结果。
|
|
3663
|
+
* 打开后先在框内显示旋转图标与醒目的加载文案,内容就绪后再显示 iframe。
|
|
3168
3664
|
*/
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3665
|
+
function PreviewFrame({ t, src, height, onClose }) {
|
|
3666
|
+
const [loadedSrc, setLoadedSrc] = (0, react.useState)("");
|
|
3667
|
+
const loaded = loadedSrc === src;
|
|
3668
|
+
const title = t("pdf.fulltext");
|
|
3669
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3670
|
+
style: WRAP_STYLE$6,
|
|
3671
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3672
|
+
style: HEAD_STYLE$3,
|
|
3673
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3674
|
+
type: "button",
|
|
3675
|
+
style: CLOSE_STYLE,
|
|
3676
|
+
onClick: onClose,
|
|
3677
|
+
children: t("pdf.closePreview")
|
|
3678
|
+
})]
|
|
3679
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3680
|
+
style: {
|
|
3681
|
+
...BODY_STYLE$1,
|
|
3682
|
+
height
|
|
3683
|
+
},
|
|
3684
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
3685
|
+
title,
|
|
3686
|
+
src,
|
|
3687
|
+
onLoad: () => setLoadedSrc(src),
|
|
3688
|
+
style: {
|
|
3689
|
+
...IFRAME_STYLE,
|
|
3690
|
+
opacity: loaded ? 1 : 0
|
|
3691
|
+
}
|
|
3692
|
+
}, src), !loaded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3693
|
+
style: OVERLAY_STYLE,
|
|
3694
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3695
|
+
style: LOADING_TEXT_STYLE,
|
|
3696
|
+
children: t("pdf.htmlLoading")
|
|
3697
|
+
})]
|
|
3698
|
+
})]
|
|
3699
|
+
})]
|
|
3700
|
+
});
|
|
3186
3701
|
}
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
"
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3702
|
+
/** 不依赖 CSS keyframes 的旋转图标(SMIL),避免注入 <style> 受 CSP 影响。 */
|
|
3703
|
+
function Spinner() {
|
|
3704
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3705
|
+
width: 30,
|
|
3706
|
+
height: 30,
|
|
3707
|
+
viewBox: "0 0 24 24",
|
|
3708
|
+
"aria-hidden": "true",
|
|
3709
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3710
|
+
cx: "12",
|
|
3711
|
+
cy: "12",
|
|
3712
|
+
r: "9",
|
|
3713
|
+
fill: "none",
|
|
3714
|
+
stroke: "rgba(80,140,255,0.22)",
|
|
3715
|
+
strokeWidth: "3"
|
|
3716
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3717
|
+
cx: "12",
|
|
3718
|
+
cy: "12",
|
|
3719
|
+
r: "9",
|
|
3720
|
+
fill: "none",
|
|
3721
|
+
stroke: ACCENT,
|
|
3722
|
+
strokeWidth: "3",
|
|
3723
|
+
strokeLinecap: "round",
|
|
3724
|
+
strokeDasharray: "56",
|
|
3725
|
+
strokeDashoffset: "42",
|
|
3726
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("animateTransform", {
|
|
3727
|
+
attributeName: "transform",
|
|
3728
|
+
type: "rotate",
|
|
3729
|
+
from: "0 12 12",
|
|
3730
|
+
to: "360 12 12",
|
|
3731
|
+
dur: "0.9s",
|
|
3732
|
+
repeatCount: "indefinite"
|
|
3733
|
+
})
|
|
3734
|
+
})]
|
|
3735
|
+
});
|
|
3204
3736
|
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3737
|
+
const WRAP_STYLE$6 = {
|
|
3738
|
+
marginTop: 6,
|
|
3739
|
+
border: "1px solid rgba(127,127,127,0.22)",
|
|
3740
|
+
borderRadius: 6,
|
|
3741
|
+
overflow: "hidden"
|
|
3742
|
+
};
|
|
3743
|
+
const HEAD_STYLE$3 = {
|
|
3744
|
+
display: "flex",
|
|
3745
|
+
alignItems: "center",
|
|
3746
|
+
justifyContent: "space-between",
|
|
3747
|
+
gap: 8,
|
|
3748
|
+
padding: "3px 8px",
|
|
3749
|
+
fontSize: 11,
|
|
3750
|
+
background: "rgba(127,127,127,0.08)"
|
|
3751
|
+
};
|
|
3752
|
+
const CLOSE_STYLE = {
|
|
3753
|
+
padding: 0,
|
|
3754
|
+
border: "none",
|
|
3755
|
+
background: "transparent",
|
|
3756
|
+
color: ACCENT,
|
|
3757
|
+
fontSize: 12.5,
|
|
3758
|
+
cursor: "pointer"
|
|
3759
|
+
};
|
|
3760
|
+
const BODY_STYLE$1 = {
|
|
3761
|
+
position: "relative",
|
|
3762
|
+
background: "#fff",
|
|
3763
|
+
overflow: "hidden"
|
|
3764
|
+
};
|
|
3765
|
+
const IFRAME_STYLE = {
|
|
3766
|
+
display: "block",
|
|
3767
|
+
width: "100%",
|
|
3768
|
+
height: "100%",
|
|
3769
|
+
border: "none",
|
|
3770
|
+
background: "#fff",
|
|
3771
|
+
transition: "opacity 0.15s ease"
|
|
3772
|
+
};
|
|
3773
|
+
const OVERLAY_STYLE = {
|
|
3774
|
+
position: "absolute",
|
|
3775
|
+
inset: 0,
|
|
3776
|
+
display: "flex",
|
|
3777
|
+
flexDirection: "column",
|
|
3778
|
+
alignItems: "center",
|
|
3779
|
+
justifyContent: "center",
|
|
3780
|
+
gap: 10,
|
|
3781
|
+
background: "#fff"
|
|
3782
|
+
};
|
|
3783
|
+
const LOADING_TEXT_STYLE = {
|
|
3784
|
+
fontSize: 13.5,
|
|
3785
|
+
fontWeight: 700,
|
|
3786
|
+
letterSpacing: "0.02em",
|
|
3787
|
+
color: ACCENT
|
|
3788
|
+
};
|
|
3789
|
+
// src/client/ZoteroSyncDialog.tsx
|
|
3790
|
+
/** 批量同步的并发数,与批量下载保持一致。 */
|
|
3791
|
+
const SYNC_CONCURRENCY = 5;
|
|
3792
|
+
const PHASE_COLOR$1 = {
|
|
3793
|
+
pending: "rgba(127,127,127,0.9)",
|
|
3794
|
+
syncing: ACCENT,
|
|
3795
|
+
synced: "#2e9e5b",
|
|
3796
|
+
failed: ERROR
|
|
3797
|
+
};
|
|
3798
|
+
function initialRows(items, states) {
|
|
3799
|
+
return items.map((item) => {
|
|
3800
|
+
const state = states[item.paperId];
|
|
3801
|
+
const phase = state?.zoteroStatus === "synced" ? "synced" : state?.zoteroStatus === "failed" ? "failed" : "pending";
|
|
3802
|
+
return {
|
|
3803
|
+
paperId: item.paperId,
|
|
3804
|
+
title: item.title,
|
|
3805
|
+
phase,
|
|
3806
|
+
...item.source === void 0 || item.source === "" ? {} : { source: item.source },
|
|
3807
|
+
...item.meta === void 0 || item.meta === "" ? {} : { meta: item.meta },
|
|
3808
|
+
...state?.zoteroError === void 0 ? {} : { error: state.zoteroError },
|
|
3809
|
+
...phase === "synced" && state?.zoteroCollection !== void 0 ? { note: state.zoteroCollection } : {}
|
|
3810
|
+
};
|
|
3811
|
+
});
|
|
3812
|
+
}
|
|
3813
|
+
function targetLabel(t, target, currentId) {
|
|
3814
|
+
const indent = " ".repeat(Math.max(0, target.level));
|
|
3815
|
+
const suffix = target.filesEditable === false ? ` ${t("zotero.dialog.readonly")}` : target.id === currentId ? ` ${t("zotero.dialog.current")}` : "";
|
|
3816
|
+
return `${indent}${target.name}${suffix}`;
|
|
3817
|
+
}
|
|
3818
|
+
function ZoteroSyncDialog({ t, open, taskId, items, states, onClose, onDone, meta }) {
|
|
3819
|
+
const [rows, setRows] = (0, react.useState)(() => initialRows(items, states));
|
|
3820
|
+
const [targets, setTargets] = (0, react.useState)([]);
|
|
3821
|
+
const [currentId, setCurrentId] = (0, react.useState)(void 0);
|
|
3822
|
+
const [targetId, setTargetId] = (0, react.useState)("");
|
|
3823
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
3824
|
+
/** 目录列表本轮是否已读完(含失败);自动同步必须等它就绪,避免用到上一批目录。 */
|
|
3825
|
+
const [targetsReady, setTargetsReady] = (0, react.useState)(false);
|
|
3826
|
+
const [loadError, setLoadError] = (0, react.useState)(void 0);
|
|
3827
|
+
const [running, setRunning] = (0, react.useState)(false);
|
|
3828
|
+
const [notice, setNotice] = (0, react.useState)(void 0);
|
|
3829
|
+
const [hasRun, setHasRun] = (0, react.useState)(false);
|
|
3830
|
+
/** 取消后停止派发剩余文献;已发出的请求仍会完成。 */
|
|
3831
|
+
const stopRef = (0, react.useRef)(false);
|
|
3832
|
+
/** 自动同步只跑一次;每次打开/换清单时重置。 */
|
|
3833
|
+
const autoStartRef = (0, react.useRef)(false);
|
|
3834
|
+
const key = items.map((item) => item.paperId).join(",");
|
|
3835
|
+
const desiredId = meta?.initialTarget?.id;
|
|
3836
|
+
const desiredName = meta?.initialTarget?.name ?? "";
|
|
3837
|
+
(0, react.useEffect)(() => {
|
|
3838
|
+
if (!open) return;
|
|
3839
|
+
setRows(initialRows(items, states));
|
|
3840
|
+
setHasRun(false);
|
|
3841
|
+
setNotice(void 0);
|
|
3842
|
+
setLoadError(void 0);
|
|
3843
|
+
setTargetsReady(false);
|
|
3844
|
+
setTargets([]);
|
|
3845
|
+
setTargetId("");
|
|
3846
|
+
autoStartRef.current = false;
|
|
3847
|
+
let cancelled = false;
|
|
3848
|
+
setLoading(true);
|
|
3849
|
+
fetchZoteroTargets().then((result) => {
|
|
3850
|
+
if (cancelled) return;
|
|
3851
|
+
if (!result.ok) {
|
|
3852
|
+
setTargets([]);
|
|
3853
|
+
setLoadError(result.error ?? t("zotero.dialog.unknownError"));
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
setTargets(result.targets);
|
|
3857
|
+
setCurrentId(result.current?.id);
|
|
3858
|
+
const matched = desiredId === void 0 ? void 0 : result.targets.find((entry) => entry.id === desiredId);
|
|
3859
|
+
if (desiredId !== void 0 && matched === void 0) setNotice(tpl(t("zotero.dialog.modelTargetMissing"), { name: desiredName }));
|
|
3860
|
+
setTargetId(matched?.id ?? result.current?.id ?? result.targets[0]?.id ?? "");
|
|
3861
|
+
}).catch((error) => {
|
|
3862
|
+
if (cancelled) return;
|
|
3863
|
+
setTargets([]);
|
|
3864
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
3865
|
+
}).finally(() => {
|
|
3866
|
+
if (cancelled) return;
|
|
3867
|
+
setLoading(false);
|
|
3868
|
+
setTargetsReady(true);
|
|
3869
|
+
});
|
|
3870
|
+
return () => {
|
|
3871
|
+
cancelled = true;
|
|
3872
|
+
};
|
|
3873
|
+
}, [
|
|
3874
|
+
open,
|
|
3875
|
+
key,
|
|
3876
|
+
desiredId
|
|
3877
|
+
]);
|
|
3878
|
+
const target = targets.find((entry) => entry.id === targetId);
|
|
3879
|
+
const total = rows.length;
|
|
3880
|
+
const synced = rows.filter((row) => row.phase === "synced").length;
|
|
3881
|
+
const failed = rows.filter((row) => row.phase === "failed").length;
|
|
3882
|
+
const pending = rows.filter((row) => row.phase === "pending").length;
|
|
3883
|
+
const syncing = rows.filter((row) => row.phase === "syncing").length;
|
|
3884
|
+
const done = total - pending - syncing;
|
|
3885
|
+
const rerunHint = rows.some((row) => row.phase === "synced");
|
|
3886
|
+
const run = async () => {
|
|
3887
|
+
if (running || target === void 0) return;
|
|
3888
|
+
setRunning(true);
|
|
3889
|
+
setNotice(void 0);
|
|
3890
|
+
setHasRun(true);
|
|
3891
|
+
stopRef.current = false;
|
|
3892
|
+
const list = rows.map((row) => ({
|
|
3893
|
+
...row,
|
|
3894
|
+
phase: "pending",
|
|
3895
|
+
error: void 0,
|
|
3896
|
+
note: void 0
|
|
3897
|
+
}));
|
|
3898
|
+
setRows(list);
|
|
3899
|
+
const failedIds = [];
|
|
3900
|
+
const runOne = async (row) => {
|
|
3901
|
+
setRows((prev) => prev.map((entry) => entry.paperId === row.paperId ? {
|
|
3902
|
+
...entry,
|
|
3903
|
+
phase: "syncing",
|
|
3904
|
+
error: void 0
|
|
3905
|
+
} : entry));
|
|
3906
|
+
try {
|
|
3907
|
+
const result = await syncZotero(taskId, [row.paperId], {
|
|
3908
|
+
id: target.id,
|
|
3909
|
+
name: target.name,
|
|
3910
|
+
...target.filesEditable === void 0 ? {} : { filesEditable: target.filesEditable }
|
|
3911
|
+
});
|
|
3912
|
+
const item = result.items[0];
|
|
3913
|
+
const ok = item?.ok ?? result.ok;
|
|
3914
|
+
const note = item?.ok === true ? result.messages[0] : void 0;
|
|
3915
|
+
if (!ok) failedIds.push(row.paperId);
|
|
3916
|
+
setRows((prev) => prev.map((entry) => entry.paperId === row.paperId ? {
|
|
3917
|
+
...entry,
|
|
3918
|
+
phase: ok ? "synced" : "failed",
|
|
3919
|
+
...ok ? note === void 0 ? {} : { note } : { error: item?.error ?? result.messages[0] ?? t("zotero.dialog.failFallback") }
|
|
3920
|
+
} : entry));
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3923
|
+
stopRef.current = true;
|
|
3924
|
+
failedIds.push(row.paperId);
|
|
3925
|
+
setRows((prev) => prev.map((entry) => entry.paperId === row.paperId ? {
|
|
3926
|
+
...entry,
|
|
3927
|
+
phase: "failed",
|
|
3928
|
+
error: message
|
|
3929
|
+
} : entry));
|
|
3930
|
+
setNotice(message);
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
let cursor = 0;
|
|
3934
|
+
const worker = async () => {
|
|
3935
|
+
while (!stopRef.current && cursor < list.length) {
|
|
3936
|
+
const row = list[cursor];
|
|
3937
|
+
cursor += 1;
|
|
3938
|
+
if (row !== void 0) await runOne(row);
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
await Promise.all(Array.from({ length: Math.min(SYNC_CONCURRENCY, list.length) }, () => worker()));
|
|
3942
|
+
setRunning(false);
|
|
3943
|
+
onDone(failedIds);
|
|
3944
|
+
};
|
|
3945
|
+
(0, react.useEffect)(() => {
|
|
3946
|
+
if (!open || loading || !targetsReady || meta?.autoStart !== true || autoStartRef.current) return;
|
|
3947
|
+
if (running) return;
|
|
3948
|
+
if (desiredId === void 0) {
|
|
3949
|
+
autoStartRef.current = true;
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
if (target === void 0 || target.id !== desiredId) {
|
|
3953
|
+
if (loadError !== void 0 || targets.length === 0) {
|
|
3954
|
+
autoStartRef.current = true;
|
|
3955
|
+
setNotice(t("zotero.dialog.modelNoAuto"));
|
|
3956
|
+
} else if (!targets.some((entry) => entry.id === desiredId)) autoStartRef.current = true;
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
autoStartRef.current = true;
|
|
3960
|
+
run();
|
|
3961
|
+
}, [
|
|
3962
|
+
open,
|
|
3963
|
+
loading,
|
|
3964
|
+
targetsReady,
|
|
3965
|
+
running,
|
|
3966
|
+
target,
|
|
3967
|
+
targets,
|
|
3968
|
+
loadError,
|
|
3969
|
+
desiredId,
|
|
3970
|
+
meta?.autoStart
|
|
3971
|
+
]);
|
|
3972
|
+
const summary = running ? t("zotero.dialog.running") : done > 0 && failed === 0 && pending === 0 ? tpl(t("zotero.dialog.done"), { n: synced }) : done > 0 ? tpl(t("zotero.dialog.partial"), {
|
|
3973
|
+
saved: synced,
|
|
3974
|
+
failed
|
|
3975
|
+
}) : void 0;
|
|
3976
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
3977
|
+
open,
|
|
3978
|
+
onClose: () => {
|
|
3979
|
+
if (!running) onClose();
|
|
3980
|
+
},
|
|
3981
|
+
title: meta === void 0 ? t("zotero.dialog.title") : t("zotero.dialog.modelTitle"),
|
|
3982
|
+
description: tpl(meta === void 0 ? t("zotero.dialog.desc") : meta.autoStart ? t("zotero.dialog.modelDescAuto") : t("zotero.dialog.modelDescPick"), { n: total }),
|
|
3983
|
+
className: WIDE_MODAL_CLASS$2,
|
|
3984
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3985
|
+
style: FOOTER_STYLE$1,
|
|
3986
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3987
|
+
style: statusStyle,
|
|
3988
|
+
children: tpl(t("zotero.dialog.progress"), {
|
|
3989
|
+
done,
|
|
3990
|
+
total
|
|
3991
|
+
})
|
|
3992
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3993
|
+
style: ACTIONS_STYLE,
|
|
3994
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3995
|
+
variant: "outline",
|
|
3996
|
+
size: "sm",
|
|
3997
|
+
type: "button",
|
|
3998
|
+
onClick: () => {
|
|
3999
|
+
if (running) stopRef.current = true;
|
|
4000
|
+
else onClose();
|
|
4001
|
+
},
|
|
4002
|
+
children: running ? t("zotero.dialog.cancel") : t("zotero.dialog.close")
|
|
4003
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4004
|
+
variant: "primary",
|
|
4005
|
+
size: "sm",
|
|
4006
|
+
type: "button",
|
|
4007
|
+
onClick: run,
|
|
4008
|
+
disabled: running || loading || target === void 0,
|
|
4009
|
+
children: running ? t("zotero.dialog.syncing") : hasRun || rerunHint ? t("zotero.dialog.resync") : t("zotero.dialog.sync")
|
|
4010
|
+
})]
|
|
4011
|
+
})]
|
|
4012
|
+
}),
|
|
4013
|
+
children: [
|
|
4014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS$2 }),
|
|
4015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4016
|
+
style: TARGET_BOX_STYLE,
|
|
4017
|
+
children: [
|
|
4018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4019
|
+
style: TARGET_LABEL_STYLE,
|
|
4020
|
+
htmlFor: "litsearch-zotero-target",
|
|
4021
|
+
children: t("zotero.dialog.target")
|
|
4022
|
+
}),
|
|
4023
|
+
loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4024
|
+
style: MUTED_STYLE,
|
|
4025
|
+
children: t("zotero.dialog.loading")
|
|
4026
|
+
}) : loadError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4027
|
+
style: ERROR_TEXT_STYLE,
|
|
4028
|
+
title: loadError,
|
|
4029
|
+
children: tpl(t("zotero.dialog.loadFail"), { message: loadError })
|
|
4030
|
+
}) : targets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4031
|
+
style: MUTED_STYLE,
|
|
4032
|
+
children: t("zotero.dialog.noTarget")
|
|
4033
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
4034
|
+
id: "litsearch-zotero-target",
|
|
4035
|
+
style: SELECT_STYLE$4,
|
|
4036
|
+
value: targetId,
|
|
4037
|
+
onChange: (event) => setTargetId(event.target.value),
|
|
4038
|
+
disabled: running,
|
|
4039
|
+
children: targets.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4040
|
+
value: entry.id,
|
|
4041
|
+
children: targetLabel(t, entry, currentId)
|
|
4042
|
+
}, entry.id))
|
|
4043
|
+
}),
|
|
4044
|
+
meta?.initialTarget !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4045
|
+
style: HINT_STYLE$2,
|
|
4046
|
+
children: tpl(t("zotero.dialog.modelTarget"), { name: meta.initialTarget.name })
|
|
4047
|
+
}),
|
|
4048
|
+
rerunHint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4049
|
+
style: HINT_STYLE$2,
|
|
4050
|
+
children: t("zotero.dialog.rerunHint")
|
|
4051
|
+
})
|
|
4052
|
+
]
|
|
4053
|
+
}),
|
|
4054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4055
|
+
style: OVERVIEW_STYLE$1,
|
|
4056
|
+
role: "status",
|
|
4057
|
+
"aria-live": "polite",
|
|
4058
|
+
children: [
|
|
4059
|
+
meta !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4060
|
+
style: MODEL_BADGE_STYLE$1,
|
|
4061
|
+
children: [
|
|
4062
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4063
|
+
style: MODEL_TAG_STYLE$1,
|
|
4064
|
+
children: t("zotero.dialog.originModel")
|
|
4065
|
+
}),
|
|
4066
|
+
meta.scopeLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: meta.scopeLabel }),
|
|
4067
|
+
meta.reason !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4068
|
+
style: REASON_STYLE$1,
|
|
4069
|
+
title: meta.reason,
|
|
4070
|
+
children: meta.reason
|
|
4071
|
+
})
|
|
4072
|
+
]
|
|
4073
|
+
}),
|
|
4074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4075
|
+
style: COUNTS_STYLE$1,
|
|
4076
|
+
children: [
|
|
4077
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("zotero.dialog.total"), { n: total }) }),
|
|
4078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4079
|
+
style: { color: PHASE_COLOR$1.synced },
|
|
4080
|
+
children: tpl(t("zotero.dialog.synced"), { n: synced })
|
|
4081
|
+
}),
|
|
4082
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4083
|
+
style: { color: PHASE_COLOR$1.syncing },
|
|
4084
|
+
children: tpl(t("zotero.dialog.syncingCount"), { n: syncing })
|
|
4085
|
+
}),
|
|
4086
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("zotero.dialog.pending"), { n: pending }) }),
|
|
4087
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4088
|
+
style: { color: PHASE_COLOR$1.failed },
|
|
4089
|
+
children: tpl(t("zotero.dialog.failed"), { n: failed })
|
|
4090
|
+
})
|
|
4091
|
+
]
|
|
4092
|
+
}),
|
|
4093
|
+
summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4094
|
+
style: {
|
|
4095
|
+
...SUMMARY_STYLE$1,
|
|
4096
|
+
color: failed > 0 ? "#d64545" : "#2e9e5b"
|
|
4097
|
+
},
|
|
4098
|
+
children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpinnerIcon, {}), summary]
|
|
4099
|
+
}),
|
|
4100
|
+
notice !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4101
|
+
style: ERROR_TEXT_STYLE,
|
|
4102
|
+
title: notice,
|
|
4103
|
+
children: notice
|
|
4104
|
+
})
|
|
4105
|
+
]
|
|
4106
|
+
}),
|
|
4107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4108
|
+
style: LIST_STYLE$4,
|
|
4109
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4110
|
+
style: ROW_STYLE$5,
|
|
4111
|
+
children: [
|
|
4112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4113
|
+
style: INDEX_STYLE$1,
|
|
4114
|
+
children: index + 1
|
|
4115
|
+
}),
|
|
4116
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4117
|
+
style: ROW_MAIN_STYLE$2,
|
|
4118
|
+
children: [
|
|
4119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4120
|
+
style: ROW_TITLE_STYLE$2,
|
|
4121
|
+
title: row.title,
|
|
4122
|
+
children: row.title === "" ? t("zotero.dialog.untitled") : row.title
|
|
4123
|
+
}),
|
|
4124
|
+
row.source !== void 0 && row.source !== "" || row.meta !== void 0 && row.meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4125
|
+
style: ROW_META_STYLE$3,
|
|
4126
|
+
children: [row.source === void 0 || row.source === "" ? "" : tpl(t("zotero.dialog.source"), { source: row.source }), row.meta ?? ""].filter((part) => part !== "").join(" · ")
|
|
4127
|
+
}) : null,
|
|
4128
|
+
row.error !== void 0 && row.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4129
|
+
style: ROW_ERROR_STYLE$2,
|
|
4130
|
+
title: row.error,
|
|
4131
|
+
children: row.error
|
|
4132
|
+
}),
|
|
4133
|
+
row.error === void 0 && row.note !== void 0 && row.note !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4134
|
+
style: ROW_NOTE_STYLE,
|
|
4135
|
+
title: row.note,
|
|
4136
|
+
children: row.note
|
|
4137
|
+
})
|
|
4138
|
+
]
|
|
4139
|
+
}),
|
|
4140
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4141
|
+
style: {
|
|
4142
|
+
...STATUS_CHIP_STYLE$1,
|
|
4143
|
+
color: PHASE_COLOR$1[row.phase]
|
|
4144
|
+
},
|
|
4145
|
+
children: [row.phase === "syncing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpinnerIcon, {}), t(`zotero.dialog.status.${row.phase}`)]
|
|
4146
|
+
})
|
|
4147
|
+
]
|
|
4148
|
+
}, row.paperId))
|
|
4149
|
+
})
|
|
4150
|
+
]
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
const WIDE_MODAL_CLASS$2 = "litsearch-zotero-modal";
|
|
4154
|
+
const WIDE_MODAL_CSS$2 = `
|
|
4155
|
+
.${WIDE_MODAL_CLASS$2}{width:min(760px,calc(100vw - 48px))}
|
|
4156
|
+
`;
|
|
4157
|
+
const FOOTER_STYLE$1 = {
|
|
4158
|
+
display: "flex",
|
|
4159
|
+
alignItems: "center",
|
|
4160
|
+
justifyContent: "space-between",
|
|
4161
|
+
gap: 8
|
|
4162
|
+
};
|
|
4163
|
+
const ACTIONS_STYLE = {
|
|
4164
|
+
display: "flex",
|
|
4165
|
+
alignItems: "center",
|
|
4166
|
+
gap: 8
|
|
4167
|
+
};
|
|
4168
|
+
const TARGET_BOX_STYLE = {
|
|
4169
|
+
display: "flex",
|
|
4170
|
+
flexDirection: "column",
|
|
4171
|
+
gap: 6,
|
|
4172
|
+
marginBottom: 10,
|
|
4173
|
+
padding: "8px 10px",
|
|
4174
|
+
borderRadius: 8,
|
|
4175
|
+
border: "1px solid rgba(80,140,255,0.25)",
|
|
4176
|
+
background: "rgba(80,140,255,0.06)"
|
|
4177
|
+
};
|
|
4178
|
+
const TARGET_LABEL_STYLE = {
|
|
4179
|
+
fontSize: 11,
|
|
4180
|
+
fontWeight: 650,
|
|
4181
|
+
opacity: .7
|
|
4182
|
+
};
|
|
4183
|
+
const SELECT_STYLE$4 = {
|
|
4184
|
+
width: "100%",
|
|
4185
|
+
padding: "4px 6px",
|
|
4186
|
+
fontSize: 12.5,
|
|
4187
|
+
borderRadius: 6,
|
|
4188
|
+
border: "1px solid rgba(127,127,127,0.35)",
|
|
4189
|
+
background: "transparent",
|
|
4190
|
+
color: "inherit"
|
|
4191
|
+
};
|
|
4192
|
+
const HINT_STYLE$2 = {
|
|
4193
|
+
fontSize: 11,
|
|
4194
|
+
lineHeight: 1.5,
|
|
4195
|
+
opacity: .7
|
|
4196
|
+
};
|
|
4197
|
+
const OVERVIEW_STYLE$1 = {
|
|
4198
|
+
display: "flex",
|
|
4199
|
+
flexDirection: "column",
|
|
4200
|
+
gap: 6,
|
|
4201
|
+
marginBottom: 8,
|
|
4202
|
+
padding: "8px 10px",
|
|
4203
|
+
borderRadius: 8,
|
|
4204
|
+
border: "1px solid rgba(80,140,255,0.25)",
|
|
4205
|
+
background: "rgba(80,140,255,0.08)"
|
|
4206
|
+
};
|
|
4207
|
+
const COUNTS_STYLE$1 = {
|
|
4208
|
+
display: "flex",
|
|
4209
|
+
flexWrap: "wrap",
|
|
4210
|
+
gap: 10,
|
|
4211
|
+
fontSize: 12,
|
|
4212
|
+
lineHeight: 1.5
|
|
4213
|
+
};
|
|
4214
|
+
const SUMMARY_STYLE$1 = {
|
|
4215
|
+
display: "inline-flex",
|
|
4216
|
+
alignItems: "center",
|
|
4217
|
+
gap: 4,
|
|
4218
|
+
fontSize: 12,
|
|
4219
|
+
fontWeight: 650
|
|
4220
|
+
};
|
|
4221
|
+
const MUTED_STYLE = {
|
|
4222
|
+
fontSize: 12,
|
|
4223
|
+
opacity: .7
|
|
4224
|
+
};
|
|
4225
|
+
const ERROR_TEXT_STYLE = {
|
|
4226
|
+
fontSize: 11.5,
|
|
4227
|
+
lineHeight: 1.5,
|
|
4228
|
+
color: ERROR,
|
|
4229
|
+
overflow: "hidden",
|
|
4230
|
+
display: "-webkit-box",
|
|
4231
|
+
WebkitLineClamp: 2,
|
|
4232
|
+
WebkitBoxOrient: "vertical",
|
|
4233
|
+
wordBreak: "break-word"
|
|
4234
|
+
};
|
|
4235
|
+
const LIST_STYLE$4 = {
|
|
4236
|
+
listStyle: "none",
|
|
4237
|
+
margin: 0,
|
|
4238
|
+
padding: 0,
|
|
4239
|
+
maxHeight: "46vh",
|
|
4240
|
+
overflowY: "auto"
|
|
4241
|
+
};
|
|
4242
|
+
const ROW_STYLE$5 = {
|
|
4243
|
+
display: "flex",
|
|
4244
|
+
alignItems: "flex-start",
|
|
4245
|
+
gap: 8,
|
|
4246
|
+
padding: "7px 0",
|
|
4247
|
+
borderTop: "1px solid rgba(127,127,127,0.18)"
|
|
4248
|
+
};
|
|
4249
|
+
const INDEX_STYLE$1 = {
|
|
4250
|
+
flexShrink: 0,
|
|
4251
|
+
minWidth: 18,
|
|
4252
|
+
fontSize: 11,
|
|
4253
|
+
lineHeight: "18px",
|
|
4254
|
+
opacity: .5,
|
|
4255
|
+
textAlign: "right"
|
|
4256
|
+
};
|
|
4257
|
+
const ROW_MAIN_STYLE$2 = {
|
|
4258
|
+
display: "flex",
|
|
4259
|
+
flexDirection: "column",
|
|
4260
|
+
gap: 2,
|
|
4261
|
+
flex: 1,
|
|
4262
|
+
minWidth: 0
|
|
4263
|
+
};
|
|
4264
|
+
const ROW_TITLE_STYLE$2 = {
|
|
4265
|
+
fontSize: 12.5,
|
|
4266
|
+
lineHeight: 1.45,
|
|
4267
|
+
fontWeight: 600,
|
|
4268
|
+
overflow: "hidden",
|
|
4269
|
+
display: "-webkit-box",
|
|
4270
|
+
WebkitLineClamp: 2,
|
|
4271
|
+
WebkitBoxOrient: "vertical",
|
|
4272
|
+
wordBreak: "break-word"
|
|
4273
|
+
};
|
|
4274
|
+
const ROW_ERROR_STYLE$2 = {
|
|
4275
|
+
fontSize: 11,
|
|
4276
|
+
lineHeight: 1.4,
|
|
4277
|
+
color: ERROR,
|
|
4278
|
+
opacity: .85,
|
|
4279
|
+
overflow: "hidden",
|
|
4280
|
+
display: "-webkit-box",
|
|
4281
|
+
WebkitLineClamp: 2,
|
|
4282
|
+
WebkitBoxOrient: "vertical",
|
|
4283
|
+
wordBreak: "break-word"
|
|
4284
|
+
};
|
|
4285
|
+
const ROW_NOTE_STYLE = {
|
|
4286
|
+
fontSize: 11,
|
|
4287
|
+
lineHeight: 1.4,
|
|
4288
|
+
opacity: .65,
|
|
4289
|
+
overflow: "hidden",
|
|
4290
|
+
display: "-webkit-box",
|
|
4291
|
+
WebkitLineClamp: 1,
|
|
4292
|
+
WebkitBoxOrient: "vertical",
|
|
4293
|
+
wordBreak: "break-word"
|
|
4294
|
+
};
|
|
4295
|
+
const MODEL_BADGE_STYLE$1 = {
|
|
4296
|
+
display: "flex",
|
|
4297
|
+
flexWrap: "wrap",
|
|
4298
|
+
alignItems: "center",
|
|
4299
|
+
gap: 8,
|
|
4300
|
+
fontSize: 12,
|
|
4301
|
+
lineHeight: 1.5,
|
|
4302
|
+
paddingBottom: 4,
|
|
4303
|
+
borderBottom: "1px solid rgba(127,127,127,0.18)"
|
|
4304
|
+
};
|
|
4305
|
+
const MODEL_TAG_STYLE$1 = {
|
|
4306
|
+
flexShrink: 0,
|
|
4307
|
+
padding: "1px 6px",
|
|
4308
|
+
borderRadius: 4,
|
|
4309
|
+
border: `1px solid ${ACCENT}`,
|
|
4310
|
+
color: ACCENT,
|
|
4311
|
+
fontWeight: 650,
|
|
4312
|
+
fontSize: 11
|
|
4313
|
+
};
|
|
4314
|
+
const REASON_STYLE$1 = {
|
|
4315
|
+
opacity: .75,
|
|
4316
|
+
overflow: "hidden",
|
|
4317
|
+
textOverflow: "ellipsis",
|
|
4318
|
+
whiteSpace: "nowrap",
|
|
4319
|
+
maxWidth: "100%"
|
|
4320
|
+
};
|
|
4321
|
+
const ROW_META_STYLE$3 = {
|
|
4322
|
+
fontSize: 11,
|
|
4323
|
+
lineHeight: 1.4,
|
|
4324
|
+
opacity: .65,
|
|
4325
|
+
overflow: "hidden",
|
|
4326
|
+
textOverflow: "ellipsis",
|
|
4327
|
+
whiteSpace: "nowrap"
|
|
4328
|
+
};
|
|
4329
|
+
const STATUS_CHIP_STYLE$1 = {
|
|
4330
|
+
display: "inline-flex",
|
|
4331
|
+
alignItems: "center",
|
|
4332
|
+
gap: 3,
|
|
4333
|
+
flexShrink: 0,
|
|
4334
|
+
padding: "1px 6px",
|
|
4335
|
+
borderRadius: 4,
|
|
4336
|
+
border: "1px solid currentColor",
|
|
4337
|
+
fontSize: 11,
|
|
4338
|
+
lineHeight: 1.5,
|
|
4339
|
+
whiteSpace: "nowrap"
|
|
4340
|
+
};
|
|
4341
|
+
// src/client/PaperPane.tsx
|
|
4342
|
+
/**
|
|
4343
|
+
* 独立论文 tab:`dsh-resource://litsearch-paper/<taskId>/<paperId>`。
|
|
4344
|
+
*
|
|
4345
|
+
* 展示完整摘要、标识符、筛读与分组、全文预览和 PubTator 实体;数据来自
|
|
4346
|
+
* 单篇详情端点,避免为了打开一篇论文拉取整页结果。
|
|
4347
|
+
*/
|
|
4348
|
+
const PAPER_ADDRESS_PREFIX = "dsh-resource://litsearch-paper/";
|
|
4349
|
+
function paperRefFromAddress(address) {
|
|
4350
|
+
if (!address.startsWith("dsh-resource://litsearch-paper/")) return null;
|
|
4351
|
+
const rest = address.slice(31);
|
|
4352
|
+
const slash = rest.indexOf("/");
|
|
4353
|
+
if (slash <= 0) return null;
|
|
4354
|
+
const taskPart = rest.slice(0, slash);
|
|
4355
|
+
const paperPart = rest.slice(slash + 1);
|
|
4356
|
+
if (paperPart === "" || paperPart.includes("/")) return null;
|
|
4357
|
+
try {
|
|
4358
|
+
return {
|
|
4359
|
+
taskId: decodeURIComponent(taskPart),
|
|
4360
|
+
paperId: decodeURIComponent(paperPart)
|
|
4361
|
+
};
|
|
4362
|
+
} catch {
|
|
4363
|
+
return null;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
const SCREENINGS$3 = [
|
|
4367
|
+
"unread",
|
|
4368
|
+
"read",
|
|
4369
|
+
"included",
|
|
4370
|
+
"excluded"
|
|
4371
|
+
];
|
|
4372
|
+
const PDF_COLOR$1 = {
|
|
4373
|
+
unknown: "rgba(127,127,127,0.8)",
|
|
4374
|
+
available: "#508cff",
|
|
4375
|
+
downloaded: "#2e9e5b",
|
|
4376
|
+
missing: "#d64545",
|
|
4377
|
+
"html-only": "#b26a00",
|
|
4378
|
+
paywalled: "#b26a00",
|
|
4379
|
+
blocked: "#d64545",
|
|
4380
|
+
failed: "#d64545"
|
|
4381
|
+
};
|
|
4382
|
+
function messageOf$3(error) {
|
|
4383
|
+
return error instanceof Error ? error.message : String(error);
|
|
4384
|
+
}
|
|
4385
|
+
function PaperPane({ t, useTabInfo, inputActions, onOpenLocal, onRevealLocal }) {
|
|
4386
|
+
const info = useTabInfo();
|
|
4387
|
+
const ref = paperRefFromAddress(info.tab.navigation?.address || info.tab.contentId || "");
|
|
4388
|
+
const [detail, setDetail] = (0, react.useState)(null);
|
|
4389
|
+
const [loading, setLoading] = (0, react.useState)(true);
|
|
4390
|
+
const [error, setError] = (0, react.useState)(null);
|
|
4391
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
4392
|
+
const [downloading, setDownloading] = (0, react.useState)(false);
|
|
4393
|
+
const [nonce, setNonce] = (0, react.useState)(0);
|
|
4394
|
+
const [toast, setToastState] = (0, react.useState)("");
|
|
4395
|
+
const [toastTone, setToastTone] = (0, react.useState)("info");
|
|
4396
|
+
const notify = (text, tone = "info") => {
|
|
4397
|
+
setToastState(text);
|
|
4398
|
+
setToastTone(tone);
|
|
4399
|
+
};
|
|
4400
|
+
const [showHtml, setShowHtml] = (0, react.useState)(false);
|
|
4401
|
+
const [zoteroOpen, setZoteroOpen] = (0, react.useState)(false);
|
|
4402
|
+
const [risFallback, setRisFallback] = (0, react.useState)(null);
|
|
4403
|
+
const taskId = ref?.taskId ?? "";
|
|
4404
|
+
ref?.paperId;
|
|
4405
|
+
(0, react.useEffect)(() => {
|
|
4406
|
+
if (ref === null) {
|
|
4407
|
+
setLoading(false);
|
|
4408
|
+
setError(t("paperTab.notFound"));
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
const controller = new AbortController();
|
|
4412
|
+
setLoading(true);
|
|
4413
|
+
fetchPaper(ref.taskId, ref.paperId, controller.signal).then((next) => {
|
|
4414
|
+
if (controller.signal.aborted) return;
|
|
3232
4415
|
setDetail(next);
|
|
3233
4416
|
setError(null);
|
|
3234
4417
|
setLoading(false);
|
|
@@ -3245,27 +4428,20 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3245
4428
|
nonce,
|
|
3246
4429
|
t
|
|
3247
4430
|
]);
|
|
3248
|
-
const state = detail?.state;
|
|
3249
|
-
const noteKey = state?.note ?? "";
|
|
3250
|
-
const tagsKey = (state?.tags ?? []).join(",");
|
|
3251
|
-
const orderKey = state?.shortlistOrder ?? "";
|
|
3252
4431
|
(0, react.useEffect)(() => {
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
orderKey
|
|
3260
|
-
]);
|
|
4432
|
+
if (ref === null) return void 0;
|
|
4433
|
+
return subscribeTaskSummary((summary) => {
|
|
4434
|
+
if (summary.id === ref.taskId) setNonce((value) => value + 1);
|
|
4435
|
+
});
|
|
4436
|
+
}, [ref?.taskId]);
|
|
4437
|
+
const state = detail?.state;
|
|
3261
4438
|
(0, react.useEffect)(() => {
|
|
3262
|
-
|
|
3263
|
-
setPreview(null);
|
|
4439
|
+
setShowHtml(false);
|
|
3264
4440
|
setRisFallback(null);
|
|
3265
4441
|
}, [ref?.taskId, ref?.paperId]);
|
|
3266
4442
|
(0, react.useEffect)(() => {
|
|
3267
4443
|
if (toast === "") return void 0;
|
|
3268
|
-
const timer = setTimeout(() =>
|
|
4444
|
+
const timer = setTimeout(() => setToastState(""), 4e3);
|
|
3269
4445
|
return () => clearTimeout(timer);
|
|
3270
4446
|
}, [toast]);
|
|
3271
4447
|
const paper = detail?.paper;
|
|
@@ -3293,62 +4469,38 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3293
4469
|
const annotate = (input) => {
|
|
3294
4470
|
if (busy) return;
|
|
3295
4471
|
setBusy(true);
|
|
3296
|
-
annotateTask(taskId, [input]).then(() => setNonce((value) => value + 1)).catch((err) =>
|
|
3297
|
-
};
|
|
3298
|
-
const saveDetail = () => {
|
|
3299
|
-
const trimmedOrder = order.trim();
|
|
3300
|
-
const parsedOrder = Number(trimmedOrder);
|
|
3301
|
-
const tagList = tags.split(/[,,;;]/u).map((tag) => tag.trim()).filter((tag) => tag !== "");
|
|
3302
|
-
annotate({
|
|
3303
|
-
paperId: paper.paperId,
|
|
3304
|
-
note,
|
|
3305
|
-
tags: tagList,
|
|
3306
|
-
shortlistOrder: trimmedOrder === "" || !Number.isFinite(parsedOrder) ? null : Math.floor(parsedOrder)
|
|
3307
|
-
});
|
|
3308
|
-
setEditing(false);
|
|
4472
|
+
annotateTask(taskId, [input]).then(() => setNonce((value) => value + 1)).catch((err) => notify(messageOf$3(err))).finally(() => setBusy(false));
|
|
3309
4473
|
};
|
|
3310
4474
|
const insert = () => {
|
|
3311
4475
|
const outcome = insertQuote(inputActions, paperText(t, paper), paperSummary(t, paper));
|
|
3312
|
-
if (outcome === "inserted")
|
|
3313
|
-
else
|
|
4476
|
+
if (outcome === "inserted") notify(t("result.inserted"));
|
|
4477
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
3314
4478
|
};
|
|
3315
4479
|
const download = () => {
|
|
3316
4480
|
setBusy(true);
|
|
4481
|
+
setDownloading(true);
|
|
3317
4482
|
downloadPaperPdf(taskId, paper.paperId).then((result) => {
|
|
3318
|
-
if (result.status === "downloaded")
|
|
3319
|
-
else if (result.status === "html-only")
|
|
3320
|
-
else
|
|
3321
|
-
}).catch((err) =>
|
|
4483
|
+
if (result.status === "downloaded") notify(tpl(t("pdf.downloaded"), { name: result.filename ?? "" }));
|
|
4484
|
+
else if (result.status === "html-only") notify(tpl(t("pdf.htmlFallback"), { message: result.error ?? "" }), "error");
|
|
4485
|
+
else notify(tpl(t("pdf.error"), { message: result.error ?? result.status }), "error");
|
|
4486
|
+
}).catch((err) => notify(tpl(t("pdf.error"), { message: messageOf$3(err) }), "error")).finally(() => {
|
|
3322
4487
|
setBusy(false);
|
|
4488
|
+
setDownloading(false);
|
|
3323
4489
|
setNonce((value) => value + 1);
|
|
3324
4490
|
});
|
|
3325
4491
|
};
|
|
3326
4492
|
const zotero = () => {
|
|
3327
|
-
|
|
3328
|
-
syncZotero(taskId, [paper.paperId]).then((result) => {
|
|
3329
|
-
const message = result.messages[0] ?? "";
|
|
3330
|
-
if (result.failed === 0 && message === "") setToast(tpl(t("pdf.zoteroDone"), { n: result.saved }));
|
|
3331
|
-
else setToast(tpl(t("pdf.zoteroPartial"), {
|
|
3332
|
-
saved: result.saved,
|
|
3333
|
-
failed: result.failed,
|
|
3334
|
-
message
|
|
3335
|
-
}));
|
|
3336
|
-
if (result.failed > 0) setRisFallback({
|
|
3337
|
-
ids: [paper.paperId],
|
|
3338
|
-
message: message || t("pdf.zoteroFailed")
|
|
3339
|
-
});
|
|
3340
|
-
}).catch((err) => {
|
|
3341
|
-
const message = messageOf$3(err);
|
|
3342
|
-
setToast(tpl(t("pdf.zoteroFailed"), { message }));
|
|
3343
|
-
setRisFallback({
|
|
3344
|
-
ids: [paper.paperId],
|
|
3345
|
-
message
|
|
3346
|
-
});
|
|
3347
|
-
}).finally(() => setBusy(false));
|
|
4493
|
+
setZoteroOpen(true);
|
|
3348
4494
|
};
|
|
3349
4495
|
const pdfStatus = state?.pdfStatus ?? (paper.pdfUrl === "" ? "unknown" : "available");
|
|
3350
4496
|
const canPdf = paper.pdfUrl !== "" || paper.ids.pmcid !== "" || paper.ids.arxivId !== "" || paper.doi !== "";
|
|
3351
4497
|
const canHtml = paper.ids.pmcid !== "";
|
|
4498
|
+
const downloaded = !downloading && state?.pdfStatus === "downloaded";
|
|
4499
|
+
const downloadedPath = downloaded ? state?.pdfFile : void 0;
|
|
4500
|
+
const pdfMissing = !downloading && state?.pdfStatus === "missing";
|
|
4501
|
+
const failedMessage = !downloading && state?.pdfStatus === "failed" ? state.pdfError : void 0;
|
|
4502
|
+
const downloadLabel = downloading ? t("pdf.downloading") : downloaded ? t("pdf.downloadedAction") : pdfMissing ? t("pdf.missingAction") : state?.pdfStatus === "failed" ? t("pdf.retry") : t("pdf.download");
|
|
4503
|
+
const downloadHint = downloading ? t("pdf.downloadingHint") : downloaded ? tpl(t("pdf.downloadedHint"), { path: downloadedPath ?? detail.downloadDir ?? "" }) : pdfMissing ? t("pdf.missingHint") : failedMessage !== void 0 ? tpl(t("pdf.retryHint"), { message: failedMessage }) : tpl(t("pdf.downloadHint"), { path: detail.downloadDir ?? t("pdf.downloadDirUnknown") });
|
|
3352
4504
|
const meta = [
|
|
3353
4505
|
paper.authors.length === 0 ? "" : `${paper.authors.slice(0, 6).join(", ")}${paper.authors.length > 6 ? " et al." : ""}`,
|
|
3354
4506
|
paper.year === null ? "" : String(paper.year),
|
|
@@ -3356,7 +4508,10 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3356
4508
|
].filter((part) => part !== "").join(" · ");
|
|
3357
4509
|
const groupName = detail.groups.find((group) => group.id === state?.groupId)?.name;
|
|
3358
4510
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3359
|
-
style:
|
|
4511
|
+
style: {
|
|
4512
|
+
...panelStyle,
|
|
4513
|
+
position: "relative"
|
|
4514
|
+
},
|
|
3360
4515
|
children: [
|
|
3361
4516
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3362
4517
|
style: {
|
|
@@ -3392,10 +4547,6 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3392
4547
|
style: badgeStyle,
|
|
3393
4548
|
children: groupName
|
|
3394
4549
|
}),
|
|
3395
|
-
state?.shortlistOrder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3396
|
-
style: badgeStyle,
|
|
3397
|
-
children: tpl(t("annotate.shortlistBadge"), { n: state.shortlistOrder })
|
|
3398
|
-
}),
|
|
3399
4550
|
sources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3400
4551
|
style: badgeStyle,
|
|
3401
4552
|
children: t(`source.${source}`)
|
|
@@ -3413,111 +4564,44 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3413
4564
|
...badgeStyle,
|
|
3414
4565
|
color: PDF_COLOR$1[pdfStatus]
|
|
3415
4566
|
},
|
|
3416
|
-
children: t(`pdf.status.${pdfStatus}`)
|
|
3417
|
-
})
|
|
3418
|
-
]
|
|
3419
|
-
}),
|
|
3420
|
-
state?.pdfError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3421
|
-
style: {
|
|
3422
|
-
...statusStyle,
|
|
3423
|
-
color: "#b26a00"
|
|
3424
|
-
},
|
|
3425
|
-
children: tpl(t("pdf.error"), { message: state.pdfError })
|
|
3426
|
-
}),
|
|
3427
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3428
|
-
style: STATE_ROW_STYLE$1,
|
|
3429
|
-
children: [
|
|
3430
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
3431
|
-
style: SELECT_STYLE$3,
|
|
3432
|
-
value: state?.screening ?? "unread",
|
|
3433
|
-
disabled: busy,
|
|
3434
|
-
"aria-label": t("filter.screening"),
|
|
3435
|
-
onChange: (event) => annotate({
|
|
3436
|
-
paperId: paper.paperId,
|
|
3437
|
-
screening: event.target.value
|
|
3438
|
-
}),
|
|
3439
|
-
children: SCREENINGS$3.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3440
|
-
value,
|
|
3441
|
-
children: t(`screening.${value}`)
|
|
3442
|
-
}, value))
|
|
3443
|
-
}),
|
|
3444
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3445
|
-
style: SELECT_STYLE$3,
|
|
3446
|
-
value: state?.groupId ?? "",
|
|
3447
|
-
disabled: busy,
|
|
3448
|
-
"aria-label": t("annotate.group"),
|
|
3449
|
-
title: detail.groups.length === 0 ? t("annotate.groupHint") : t("annotate.group"),
|
|
3450
|
-
onChange: (event) => annotate({
|
|
3451
|
-
paperId: paper.paperId,
|
|
3452
|
-
groupId: event.target.value === "" ? null : event.target.value
|
|
3453
|
-
}),
|
|
3454
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3455
|
-
value: "",
|
|
3456
|
-
children: t("annotate.noGroup")
|
|
3457
|
-
}), detail.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3458
|
-
value: group.id,
|
|
3459
|
-
children: group.name
|
|
3460
|
-
}, group.id))]
|
|
3461
|
-
}),
|
|
3462
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3463
|
-
type: "button",
|
|
3464
|
-
style: linkButtonStyle,
|
|
3465
|
-
onClick: () => setEditing((value) => !value),
|
|
3466
|
-
children: editing ? t("annotate.collapse") : t("annotate.expand")
|
|
3467
|
-
})
|
|
3468
|
-
]
|
|
3469
|
-
}),
|
|
3470
|
-
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3471
|
-
style: EDIT_STYLE$1,
|
|
3472
|
-
children: [
|
|
3473
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3474
|
-
style: FIELD_STYLE$4,
|
|
3475
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.note") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
3476
|
-
style: NOTE_STYLE$1,
|
|
3477
|
-
value: note,
|
|
3478
|
-
rows: 3,
|
|
3479
|
-
placeholder: t("annotate.notePlaceholder"),
|
|
3480
|
-
onChange: (event) => setNote(event.target.value)
|
|
3481
|
-
})]
|
|
3482
|
-
}),
|
|
3483
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3484
|
-
style: FIELD_STYLE$4,
|
|
3485
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.tags") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3486
|
-
style: INPUT_STYLE$3,
|
|
3487
|
-
value: tags,
|
|
3488
|
-
placeholder: t("annotate.tagsPlaceholder"),
|
|
3489
|
-
onChange: (event) => setTags(event.target.value)
|
|
3490
|
-
})]
|
|
3491
|
-
}),
|
|
3492
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3493
|
-
style: {
|
|
3494
|
-
...FIELD_STYLE$4,
|
|
3495
|
-
maxWidth: 120
|
|
3496
|
-
},
|
|
3497
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.shortlistOrder") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3498
|
-
style: INPUT_STYLE$3,
|
|
3499
|
-
inputMode: "numeric",
|
|
3500
|
-
value: order,
|
|
3501
|
-
placeholder: "—",
|
|
3502
|
-
onChange: (event) => setOrder(event.target.value)
|
|
3503
|
-
})]
|
|
3504
|
-
}),
|
|
3505
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3506
|
-
style: {
|
|
3507
|
-
display: "flex",
|
|
3508
|
-
justifyContent: "flex-end"
|
|
3509
|
-
},
|
|
3510
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3511
|
-
variant: "outline",
|
|
3512
|
-
size: "sm",
|
|
3513
|
-
type: "button",
|
|
3514
|
-
disabled: busy,
|
|
3515
|
-
onClick: saveDetail,
|
|
3516
|
-
children: t("annotate.save")
|
|
3517
|
-
})
|
|
4567
|
+
children: t(`pdf.status.${pdfStatus}`)
|
|
3518
4568
|
})
|
|
3519
4569
|
]
|
|
3520
4570
|
}),
|
|
4571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4572
|
+
style: STATE_ROW_STYLE$1,
|
|
4573
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
4574
|
+
style: SELECT_STYLE$3,
|
|
4575
|
+
value: state?.screening ?? "unread",
|
|
4576
|
+
disabled: busy,
|
|
4577
|
+
"aria-label": t("filter.screening"),
|
|
4578
|
+
onChange: (event) => annotate({
|
|
4579
|
+
paperId: paper.paperId,
|
|
4580
|
+
screening: event.target.value
|
|
4581
|
+
}),
|
|
4582
|
+
children: SCREENINGS$3.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4583
|
+
value,
|
|
4584
|
+
children: t(`screening.${value}`)
|
|
4585
|
+
}, value))
|
|
4586
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4587
|
+
style: SELECT_STYLE$3,
|
|
4588
|
+
value: state?.groupId ?? "",
|
|
4589
|
+
disabled: busy,
|
|
4590
|
+
"aria-label": t("annotate.group"),
|
|
4591
|
+
title: detail.groups.length === 0 ? t("annotate.groupHint") : t("annotate.group"),
|
|
4592
|
+
onChange: (event) => annotate({
|
|
4593
|
+
paperId: paper.paperId,
|
|
4594
|
+
groupId: event.target.value === "" ? null : event.target.value
|
|
4595
|
+
}),
|
|
4596
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4597
|
+
value: "",
|
|
4598
|
+
children: t("annotate.noGroup")
|
|
4599
|
+
}), detail.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4600
|
+
value: group.id,
|
|
4601
|
+
children: group.name
|
|
4602
|
+
}, group.id))]
|
|
4603
|
+
})]
|
|
4604
|
+
}),
|
|
3521
4605
|
paper.abstract !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
3522
4606
|
style: HEAD_STYLE$2,
|
|
3523
4607
|
children: t("paperTab.abstract")
|
|
@@ -3577,46 +4661,64 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3577
4661
|
paperId: paper.paperId,
|
|
3578
4662
|
pmid: paper.ids.pmid
|
|
3579
4663
|
})] }),
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
type: "button",
|
|
3586
|
-
style: linkButtonStyle,
|
|
3587
|
-
onClick: () => setPreview(null),
|
|
3588
|
-
children: t("pdf.closePreview")
|
|
3589
|
-
})]
|
|
3590
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
3591
|
-
title: preview === "pdf" ? t("pdf.preview") : t("pdf.fulltext"),
|
|
3592
|
-
src: preview === "pdf" ? paperPdfUrl(taskId, paper.paperId) : paperFullTextUrl(taskId, paper.paperId),
|
|
3593
|
-
style: IFRAME_STYLE$1
|
|
3594
|
-
})]
|
|
4664
|
+
showHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreviewFrame, {
|
|
4665
|
+
t,
|
|
4666
|
+
src: paperFullTextUrl(taskId, paper.paperId),
|
|
4667
|
+
height: 420,
|
|
4668
|
+
onClose: () => setShowHtml(false)
|
|
3595
4669
|
}),
|
|
3596
|
-
toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3597
|
-
style:
|
|
3598
|
-
children: toast
|
|
4670
|
+
(toast !== "" || risFallback !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4671
|
+
style: toastStackStyle,
|
|
4672
|
+
children: [toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4673
|
+
role: "status",
|
|
4674
|
+
style: {
|
|
4675
|
+
...toastStyle,
|
|
4676
|
+
...toastTone === "error" ? toastErrorStyle : null
|
|
4677
|
+
},
|
|
4678
|
+
children: toast
|
|
4679
|
+
}), risFallback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4680
|
+
style: FALLBACK_STYLE,
|
|
4681
|
+
children: [
|
|
4682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4683
|
+
style: { minWidth: 0 },
|
|
4684
|
+
children: tpl(t("pdf.zoteroFallback"), { message: risFallback.message })
|
|
4685
|
+
}),
|
|
4686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
4687
|
+
href: exportUrl(taskId, "ris", {}, risFallback.ids),
|
|
4688
|
+
download: true,
|
|
4689
|
+
style: FALLBACK_LINK_STYLE,
|
|
4690
|
+
children: t("pdf.zoteroFallbackLink")
|
|
4691
|
+
}),
|
|
4692
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4693
|
+
type: "button",
|
|
4694
|
+
style: FALLBACK_BUTTON_STYLE,
|
|
4695
|
+
onClick: () => setRisFallback(null),
|
|
4696
|
+
children: t("pdf.zoteroFallbackDismiss")
|
|
4697
|
+
})
|
|
4698
|
+
]
|
|
4699
|
+
})]
|
|
3599
4700
|
}),
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
4701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoteroSyncDialog, {
|
|
4702
|
+
t,
|
|
4703
|
+
open: zoteroOpen,
|
|
4704
|
+
taskId,
|
|
4705
|
+
items: [{
|
|
4706
|
+
paperId: paper.paperId,
|
|
4707
|
+
title: paper.title
|
|
4708
|
+
}],
|
|
4709
|
+
states: state === void 0 ? {} : { [paper.paperId]: state },
|
|
4710
|
+
onClose: () => {
|
|
4711
|
+
setZoteroOpen(false);
|
|
4712
|
+
setNonce((value) => value + 1);
|
|
4713
|
+
},
|
|
4714
|
+
onDone: (failedIds) => {
|
|
4715
|
+
setNonce((value) => value + 1);
|
|
4716
|
+
if (failedIds.length === 0) return;
|
|
4717
|
+
setRisFallback({
|
|
4718
|
+
ids: failedIds,
|
|
4719
|
+
message: t("zotero.dialog.failFallback")
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
3620
4722
|
}),
|
|
3621
4723
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3622
4724
|
style: ACTION_STYLE,
|
|
@@ -3628,31 +4730,38 @@ function PaperPane({ t, useTabInfo, inputActions }) {
|
|
|
3628
4730
|
onClick: insert,
|
|
3629
4731
|
children: t("result.insert")
|
|
3630
4732
|
}),
|
|
3631
|
-
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
type: "button",
|
|
3639
|
-
style: linkButtonStyle,
|
|
4733
|
+
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
|
|
4734
|
+
t,
|
|
4735
|
+
label: downloadLabel,
|
|
4736
|
+
hint: downloadHint,
|
|
4737
|
+
...downloadedPath === void 0 ? {} : { downloadedPath },
|
|
4738
|
+
missing: pdfMissing,
|
|
4739
|
+
downloading,
|
|
3640
4740
|
disabled: busy,
|
|
3641
|
-
|
|
3642
|
-
|
|
4741
|
+
style: linkButtonStyle,
|
|
4742
|
+
onDownload: download,
|
|
4743
|
+
...onOpenLocal === void 0 ? {} : { onOpen: (path) => onOpenLocal(taskId, paper.paperId, path) },
|
|
4744
|
+
...onRevealLocal === void 0 ? {} : { onReveal: onRevealLocal }
|
|
3643
4745
|
}),
|
|
3644
4746
|
canHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3645
4747
|
type: "button",
|
|
3646
4748
|
style: linkButtonStyle,
|
|
3647
|
-
onClick: () =>
|
|
4749
|
+
onClick: () => setShowHtml((value) => !value),
|
|
3648
4750
|
children: t("pdf.fulltext")
|
|
3649
4751
|
}),
|
|
3650
4752
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3651
4753
|
type: "button",
|
|
3652
|
-
style:
|
|
4754
|
+
style: state?.zoteroStatus === "synced" ? {
|
|
4755
|
+
...linkButtonStyle,
|
|
4756
|
+
color: "#2e9e5b"
|
|
4757
|
+
} : state?.zoteroStatus === "failed" ? {
|
|
4758
|
+
...linkButtonStyle,
|
|
4759
|
+
color: ERROR
|
|
4760
|
+
} : linkButtonStyle,
|
|
4761
|
+
title: state?.zoteroStatus === "synced" ? state.zoteroCollection : state?.zoteroStatus === "failed" ? state.zoteroError : void 0,
|
|
3653
4762
|
disabled: busy,
|
|
3654
4763
|
onClick: zotero,
|
|
3655
|
-
children: t("pdf.zotero")
|
|
4764
|
+
children: state?.zoteroStatus === "synced" ? t("pdf.zoteroSynced") : state?.zoteroStatus === "failed" ? t("pdf.zoteroRetry") : t("pdf.zotero")
|
|
3656
4765
|
}),
|
|
3657
4766
|
paper.pdfUrl !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3658
4767
|
style: linkStyle,
|
|
@@ -3701,73 +4810,30 @@ const SELECT_STYLE$3 = {
|
|
|
3701
4810
|
color: "inherit",
|
|
3702
4811
|
maxWidth: 150
|
|
3703
4812
|
};
|
|
3704
|
-
const EDIT_STYLE$1 = {
|
|
3705
|
-
display: "flex",
|
|
3706
|
-
flexDirection: "column",
|
|
3707
|
-
gap: 6,
|
|
3708
|
-
padding: "6px 8px",
|
|
3709
|
-
borderRadius: 6,
|
|
3710
|
-
background: "rgba(127,127,127,0.07)"
|
|
3711
|
-
};
|
|
3712
|
-
const FIELD_STYLE$4 = {
|
|
3713
|
-
display: "flex",
|
|
3714
|
-
flexDirection: "column",
|
|
3715
|
-
gap: 2,
|
|
3716
|
-
fontSize: 11,
|
|
3717
|
-
opacity: .9
|
|
3718
|
-
};
|
|
3719
|
-
const INPUT_STYLE$3 = {
|
|
3720
|
-
fontSize: 12,
|
|
3721
|
-
padding: "3px 5px",
|
|
3722
|
-
borderRadius: 4,
|
|
3723
|
-
border: "1px solid rgba(127,127,127,0.28)",
|
|
3724
|
-
background: "transparent",
|
|
3725
|
-
color: "inherit",
|
|
3726
|
-
minWidth: 0
|
|
3727
|
-
};
|
|
3728
|
-
const NOTE_STYLE$1 = {
|
|
3729
|
-
...INPUT_STYLE$3,
|
|
3730
|
-
resize: "vertical",
|
|
3731
|
-
fontFamily: "inherit",
|
|
3732
|
-
lineHeight: 1.5
|
|
3733
|
-
};
|
|
3734
4813
|
const IDS_STYLE = {
|
|
3735
4814
|
display: "flex",
|
|
3736
4815
|
flexDirection: "column",
|
|
3737
4816
|
gap: 3,
|
|
3738
4817
|
fontSize: 12
|
|
3739
4818
|
};
|
|
3740
|
-
const PREVIEW_WRAP_STYLE$1 = {
|
|
3741
|
-
border: "1px solid rgba(127,127,127,0.22)",
|
|
3742
|
-
borderRadius: 6,
|
|
3743
|
-
overflow: "hidden"
|
|
3744
|
-
};
|
|
3745
|
-
const PREVIEW_HEAD_STYLE$1 = {
|
|
3746
|
-
display: "flex",
|
|
3747
|
-
alignItems: "center",
|
|
3748
|
-
justifyContent: "space-between",
|
|
3749
|
-
gap: 8,
|
|
3750
|
-
padding: "3px 8px",
|
|
3751
|
-
fontSize: 11,
|
|
3752
|
-
background: "rgba(127,127,127,0.08)"
|
|
3753
|
-
};
|
|
3754
|
-
const IFRAME_STYLE$1 = {
|
|
3755
|
-
width: "100%",
|
|
3756
|
-
height: 420,
|
|
3757
|
-
border: "none",
|
|
3758
|
-
background: "#fff"
|
|
3759
|
-
};
|
|
3760
4819
|
const FALLBACK_STYLE = {
|
|
4820
|
+
...toastStyle,
|
|
4821
|
+
pointerEvents: "auto",
|
|
3761
4822
|
display: "flex",
|
|
3762
4823
|
alignItems: "center",
|
|
3763
4824
|
flexWrap: "wrap",
|
|
3764
4825
|
gap: 8,
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
fontSize:
|
|
3770
|
-
|
|
4826
|
+
borderLeft: "3px solid #b26a00"
|
|
4827
|
+
};
|
|
4828
|
+
const FALLBACK_LINK_STYLE = {
|
|
4829
|
+
color: "#fff",
|
|
4830
|
+
fontSize: 12,
|
|
4831
|
+
textDecoration: "underline"
|
|
4832
|
+
};
|
|
4833
|
+
const FALLBACK_BUTTON_STYLE = {
|
|
4834
|
+
...linkButtonStyle,
|
|
4835
|
+
color: "#fff",
|
|
4836
|
+
textDecoration: "underline"
|
|
3771
4837
|
};
|
|
3772
4838
|
const ACTION_STYLE = {
|
|
3773
4839
|
display: "flex",
|
|
@@ -4971,7 +6037,7 @@ function PaperRow({ paper }) {
|
|
|
4971
6037
|
paper.venue
|
|
4972
6038
|
].filter((part) => part !== "").join(" · ");
|
|
4973
6039
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4974
|
-
style: ROW_STYLE$
|
|
6040
|
+
style: ROW_STYLE$4,
|
|
4975
6041
|
children: [
|
|
4976
6042
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4977
6043
|
style: {
|
|
@@ -4981,7 +6047,7 @@ function PaperRow({ paper }) {
|
|
|
4981
6047
|
children: paper.title
|
|
4982
6048
|
}),
|
|
4983
6049
|
meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4984
|
-
style: ROW_META_STYLE$
|
|
6050
|
+
style: ROW_META_STYLE$2,
|
|
4985
6051
|
children: meta
|
|
4986
6052
|
}),
|
|
4987
6053
|
paper.doi !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
@@ -5038,14 +6104,14 @@ const LIST_STYLE$3 = {
|
|
|
5038
6104
|
display: "flex",
|
|
5039
6105
|
flexDirection: "column"
|
|
5040
6106
|
};
|
|
5041
|
-
const ROW_STYLE$
|
|
6107
|
+
const ROW_STYLE$4 = {
|
|
5042
6108
|
display: "flex",
|
|
5043
6109
|
flexDirection: "column",
|
|
5044
6110
|
gap: 1,
|
|
5045
6111
|
padding: "5px 0",
|
|
5046
6112
|
borderTop: "1px solid rgba(127,127,127,0.14)"
|
|
5047
6113
|
};
|
|
5048
|
-
const ROW_META_STYLE$
|
|
6114
|
+
const ROW_META_STYLE$2 = {
|
|
5049
6115
|
fontSize: 11,
|
|
5050
6116
|
opacity: .7
|
|
5051
6117
|
};
|
|
@@ -5149,7 +6215,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5149
6215
|
open: true,
|
|
5150
6216
|
onClose: onCancel,
|
|
5151
6217
|
title: t("condition.title"),
|
|
5152
|
-
className: WIDE_MODAL_CLASS,
|
|
6218
|
+
className: WIDE_MODAL_CLASS$1,
|
|
5153
6219
|
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5154
6220
|
style: {
|
|
5155
6221
|
display: "flex",
|
|
@@ -5171,7 +6237,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5171
6237
|
children: saving ? t("condition.saving") : t("condition.save")
|
|
5172
6238
|
})]
|
|
5173
6239
|
}),
|
|
5174
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6240
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS$1 }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5175
6241
|
style: BODY_STYLE,
|
|
5176
6242
|
children: [
|
|
5177
6243
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5213,7 +6279,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5213
6279
|
infos: health?.sources
|
|
5214
6280
|
}),
|
|
5215
6281
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5216
|
-
style: ROW_STYLE$
|
|
6282
|
+
style: ROW_STYLE$3,
|
|
5217
6283
|
children: [
|
|
5218
6284
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5219
6285
|
style: FIELD_STYLE$2,
|
|
@@ -5258,7 +6324,7 @@ function ConditionEditor({ t, detail, onSaved, onCancel }) {
|
|
|
5258
6324
|
]
|
|
5259
6325
|
}),
|
|
5260
6326
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5261
|
-
style: ROW_STYLE$
|
|
6327
|
+
style: ROW_STYLE$3,
|
|
5262
6328
|
children: [
|
|
5263
6329
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5264
6330
|
style: CHECK_STYLE$2,
|
|
@@ -5334,48 +6400,371 @@ const BODY_STYLE = {
|
|
|
5334
6400
|
overflowY: "auto"
|
|
5335
6401
|
};
|
|
5336
6402
|
/** 宿主 Modal 默认宽度 min(380px,100%),条件编辑器字段较多,单独放宽。 */
|
|
5337
|
-
const WIDE_MODAL_CLASS = "litsearch-condition-modal";
|
|
5338
|
-
const WIDE_MODAL_CSS = `
|
|
5339
|
-
.${WIDE_MODAL_CLASS}{width:min(680px,calc(100vw - 48px))}
|
|
6403
|
+
const WIDE_MODAL_CLASS$1 = "litsearch-condition-modal";
|
|
6404
|
+
const WIDE_MODAL_CSS$1 = `
|
|
6405
|
+
.${WIDE_MODAL_CLASS$1}{width:min(680px,calc(100vw - 48px))}
|
|
5340
6406
|
`;
|
|
5341
6407
|
const QUERY_LIST_STYLE = {
|
|
5342
6408
|
display: "flex",
|
|
5343
6409
|
flexDirection: "column",
|
|
5344
|
-
gap: 6
|
|
6410
|
+
gap: 6
|
|
6411
|
+
};
|
|
6412
|
+
const ROW_STYLE$3 = {
|
|
6413
|
+
display: "flex",
|
|
6414
|
+
alignItems: "center",
|
|
6415
|
+
flexWrap: "wrap",
|
|
6416
|
+
gap: 8
|
|
6417
|
+
};
|
|
6418
|
+
const FIELD_STYLE$2 = {
|
|
6419
|
+
display: "flex",
|
|
6420
|
+
alignItems: "center",
|
|
6421
|
+
gap: 4
|
|
6422
|
+
};
|
|
6423
|
+
const FIELD_LABEL_STYLE = {
|
|
6424
|
+
fontSize: 11,
|
|
6425
|
+
opacity: .68,
|
|
6426
|
+
whiteSpace: "nowrap"
|
|
6427
|
+
};
|
|
6428
|
+
const NUMBER_INPUT_STYLE = {
|
|
6429
|
+
width: 64,
|
|
6430
|
+
fontSize: 11.5
|
|
6431
|
+
};
|
|
6432
|
+
const SELECT_STYLE$1 = {
|
|
6433
|
+
fontSize: 11.5,
|
|
6434
|
+
padding: "2px 4px",
|
|
6435
|
+
borderRadius: 4,
|
|
6436
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
6437
|
+
background: "transparent",
|
|
6438
|
+
color: "inherit"
|
|
6439
|
+
};
|
|
6440
|
+
const CHECK_STYLE$2 = {
|
|
6441
|
+
display: "flex",
|
|
6442
|
+
alignItems: "center",
|
|
6443
|
+
gap: 4,
|
|
6444
|
+
fontSize: 12
|
|
6445
|
+
};
|
|
6446
|
+
// src/client/DownloadDialog.tsx
|
|
6447
|
+
const PHASE_COLOR = {
|
|
6448
|
+
pending: "rgba(127,127,127,0.9)",
|
|
6449
|
+
downloading: ACCENT,
|
|
6450
|
+
downloaded: "#2e9e5b",
|
|
6451
|
+
manual: "#b26a00",
|
|
6452
|
+
failed: ERROR
|
|
6453
|
+
};
|
|
6454
|
+
/** 单篇的展示状态:即时状态优先,其次回落到 Host 记录的文献状态。 */
|
|
6455
|
+
function phaseOf(paperId, states, downloads) {
|
|
6456
|
+
const runtime = downloads[paperId];
|
|
6457
|
+
if (runtime?.state === "failed" && isManualPdfFailure(runtime.reason)) return "manual";
|
|
6458
|
+
if (runtime?.state !== void 0) return runtime.state;
|
|
6459
|
+
const status = states[paperId]?.pdfStatus ?? "unknown";
|
|
6460
|
+
if (status === "downloaded") return "downloaded";
|
|
6461
|
+
if (status === "blocked" || status === "paywalled" || status === "html-only") return "manual";
|
|
6462
|
+
if (status === "failed" || status === "missing") return "failed";
|
|
6463
|
+
return "pending";
|
|
6464
|
+
}
|
|
6465
|
+
function errorOf(paperId, states, downloads) {
|
|
6466
|
+
return downloads[paperId]?.error ?? states[paperId]?.pdfError;
|
|
6467
|
+
}
|
|
6468
|
+
function DownloadDialog({ t, open, items, states, downloads, origin = "panel", scopeLabel, reason, cancelled = false, onCancel, onClose }) {
|
|
6469
|
+
const phases = items.map((item) => phaseOf(item.paperId, states, downloads));
|
|
6470
|
+
const downloaded = phases.filter((phase) => phase === "downloaded").length;
|
|
6471
|
+
const downloading = phases.filter((phase) => phase === "downloading").length;
|
|
6472
|
+
const manual = phases.filter((phase) => phase === "manual").length;
|
|
6473
|
+
const failed = phases.filter((phase) => phase === "failed").length;
|
|
6474
|
+
const pending = phases.filter((phase) => phase === "pending").length;
|
|
6475
|
+
const total = items.length;
|
|
6476
|
+
const done = total - pending - downloading;
|
|
6477
|
+
const rate = total === 0 ? 0 : Math.round(done / total * 100);
|
|
6478
|
+
const complete = total > 0 && pending === 0 && downloading === 0;
|
|
6479
|
+
const allSkipped = complete && downloaded === total;
|
|
6480
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
6481
|
+
open,
|
|
6482
|
+
onClose,
|
|
6483
|
+
title: origin === "model" ? t("batch.modelTitle") : t("batch.title"),
|
|
6484
|
+
description: tpl(origin === "model" ? t("batch.modelDesc") : t("batch.desc"), { n: total }),
|
|
6485
|
+
className: WIDE_MODAL_CLASS,
|
|
6486
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6487
|
+
style: FOOTER_STYLE,
|
|
6488
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6489
|
+
style: statusStyle,
|
|
6490
|
+
children: tpl(t("batch.progress"), {
|
|
6491
|
+
done,
|
|
6492
|
+
total
|
|
6493
|
+
})
|
|
6494
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6495
|
+
style: FOOTER_ACTIONS_STYLE,
|
|
6496
|
+
children: [onCancel !== void 0 && !cancelled && !complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
6497
|
+
variant: "outline",
|
|
6498
|
+
size: "sm",
|
|
6499
|
+
type: "button",
|
|
6500
|
+
onClick: onCancel,
|
|
6501
|
+
children: t("batch.cancel")
|
|
6502
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
6503
|
+
variant: "outline",
|
|
6504
|
+
size: "sm",
|
|
6505
|
+
type: "button",
|
|
6506
|
+
onClick: onClose,
|
|
6507
|
+
children: t("batch.close")
|
|
6508
|
+
})]
|
|
6509
|
+
})]
|
|
6510
|
+
}),
|
|
6511
|
+
children: [
|
|
6512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: WIDE_MODAL_CSS }),
|
|
6513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6514
|
+
style: OVERVIEW_STYLE,
|
|
6515
|
+
role: "status",
|
|
6516
|
+
"aria-live": "polite",
|
|
6517
|
+
children: [
|
|
6518
|
+
origin === "model" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6519
|
+
style: MODEL_BADGE_STYLE,
|
|
6520
|
+
children: [
|
|
6521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6522
|
+
style: MODEL_TAG_STYLE,
|
|
6523
|
+
children: t("batch.originModel")
|
|
6524
|
+
}),
|
|
6525
|
+
scopeLabel !== void 0 && scopeLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: scopeLabel }),
|
|
6526
|
+
reason !== void 0 && reason !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6527
|
+
style: REASON_STYLE,
|
|
6528
|
+
title: reason,
|
|
6529
|
+
children: reason
|
|
6530
|
+
})
|
|
6531
|
+
]
|
|
6532
|
+
}),
|
|
6533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6534
|
+
style: COUNTS_STYLE,
|
|
6535
|
+
children: [
|
|
6536
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("batch.total"), { n: total }) }),
|
|
6537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6538
|
+
style: { color: PHASE_COLOR.downloaded },
|
|
6539
|
+
children: tpl(t("batch.downloaded"), { n: downloaded })
|
|
6540
|
+
}),
|
|
6541
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6542
|
+
style: { color: PHASE_COLOR.downloading },
|
|
6543
|
+
children: tpl(t("batch.downloading"), { n: downloading })
|
|
6544
|
+
}),
|
|
6545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tpl(t("batch.pending"), { n: pending }) }),
|
|
6546
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6547
|
+
style: { color: PHASE_COLOR.manual },
|
|
6548
|
+
children: tpl(t("batch.manual"), { n: manual })
|
|
6549
|
+
}),
|
|
6550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6551
|
+
style: { color: PHASE_COLOR.failed },
|
|
6552
|
+
children: tpl(t("batch.failed"), { n: failed })
|
|
6553
|
+
})
|
|
6554
|
+
]
|
|
6555
|
+
}),
|
|
6556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6557
|
+
style: BAR_TRACK_STYLE,
|
|
6558
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
6559
|
+
...BAR_FILL_STYLE$1,
|
|
6560
|
+
width: `${rate}%`
|
|
6561
|
+
} })
|
|
6562
|
+
}),
|
|
6563
|
+
complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6564
|
+
style: COMPLETE_STYLE,
|
|
6565
|
+
children: allSkipped ? t("batch.allSkipped") : t("batch.complete")
|
|
6566
|
+
}),
|
|
6567
|
+
cancelled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6568
|
+
style: CANCELLED_STYLE,
|
|
6569
|
+
children: t("batch.cancelled")
|
|
6570
|
+
})
|
|
6571
|
+
]
|
|
6572
|
+
}),
|
|
6573
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
6574
|
+
style: LIST_STYLE$2,
|
|
6575
|
+
children: items.map((item, index) => {
|
|
6576
|
+
const phase = phases[index] ?? "pending";
|
|
6577
|
+
const error = phase === "failed" || phase === "manual" ? errorOf(item.paperId, states, downloads) : void 0;
|
|
6578
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
6579
|
+
style: ROW_STYLE$2,
|
|
6580
|
+
children: [
|
|
6581
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6582
|
+
style: INDEX_STYLE,
|
|
6583
|
+
children: index + 1
|
|
6584
|
+
}),
|
|
6585
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6586
|
+
style: ROW_MAIN_STYLE$1,
|
|
6587
|
+
children: [
|
|
6588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6589
|
+
style: ROW_TITLE_STYLE$1,
|
|
6590
|
+
title: item.title,
|
|
6591
|
+
children: item.title === "" ? t("batch.untitled") : item.title
|
|
6592
|
+
}),
|
|
6593
|
+
item.source !== void 0 && item.source !== "" || item.meta !== void 0 && item.meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6594
|
+
style: ROW_META_STYLE$1,
|
|
6595
|
+
children: [item.source === void 0 || item.source === "" ? "" : tpl(t("batch.source"), { source: item.source }), item.meta ?? ""].filter((part) => part !== "").join(" · ")
|
|
6596
|
+
}) : null,
|
|
6597
|
+
error !== void 0 && error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6598
|
+
style: ROW_ERROR_STYLE$1,
|
|
6599
|
+
title: error,
|
|
6600
|
+
children: error
|
|
6601
|
+
})
|
|
6602
|
+
]
|
|
6603
|
+
}),
|
|
6604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6605
|
+
style: {
|
|
6606
|
+
...STATUS_CHIP_STYLE,
|
|
6607
|
+
color: PHASE_COLOR[phase]
|
|
6608
|
+
},
|
|
6609
|
+
children: [phase === "downloading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpinnerIcon, {}), t(`batch.status.${phase}`)]
|
|
6610
|
+
})
|
|
6611
|
+
]
|
|
6612
|
+
}, item.paperId);
|
|
6613
|
+
})
|
|
6614
|
+
})
|
|
6615
|
+
]
|
|
6616
|
+
});
|
|
6617
|
+
}
|
|
6618
|
+
/** 批量下载弹窗的宽度约为默认弹窗的两倍(默认约 540px)。 */
|
|
6619
|
+
const WIDE_MODAL_CLASS = "litsearch-download-modal";
|
|
6620
|
+
const WIDE_MODAL_CSS = `
|
|
6621
|
+
.${WIDE_MODAL_CLASS}{width:min(1080px,calc(100vw - 48px))}
|
|
6622
|
+
`;
|
|
6623
|
+
const FOOTER_STYLE = {
|
|
6624
|
+
display: "flex",
|
|
6625
|
+
alignItems: "center",
|
|
6626
|
+
justifyContent: "space-between",
|
|
6627
|
+
gap: 8
|
|
6628
|
+
};
|
|
6629
|
+
const OVERVIEW_STYLE = {
|
|
6630
|
+
display: "flex",
|
|
6631
|
+
flexDirection: "column",
|
|
6632
|
+
gap: 6,
|
|
6633
|
+
marginBottom: 8,
|
|
6634
|
+
padding: "8px 10px",
|
|
6635
|
+
borderRadius: 8,
|
|
6636
|
+
border: "1px solid rgba(80,140,255,0.25)",
|
|
6637
|
+
background: "rgba(80,140,255,0.08)"
|
|
6638
|
+
};
|
|
6639
|
+
const COUNTS_STYLE = {
|
|
6640
|
+
display: "flex",
|
|
6641
|
+
flexWrap: "wrap",
|
|
6642
|
+
gap: 10,
|
|
6643
|
+
fontSize: 12,
|
|
6644
|
+
lineHeight: 1.5
|
|
6645
|
+
};
|
|
6646
|
+
const BAR_TRACK_STYLE = {
|
|
6647
|
+
height: 4,
|
|
6648
|
+
borderRadius: 2,
|
|
6649
|
+
background: "rgba(127,127,127,0.22)",
|
|
6650
|
+
overflow: "hidden"
|
|
6651
|
+
};
|
|
6652
|
+
const BAR_FILL_STYLE$1 = {
|
|
6653
|
+
height: "100%",
|
|
6654
|
+
borderRadius: 2,
|
|
6655
|
+
background: ACCENT,
|
|
6656
|
+
transition: "width 0.25s ease"
|
|
6657
|
+
};
|
|
6658
|
+
const COMPLETE_STYLE = {
|
|
6659
|
+
fontSize: 12,
|
|
6660
|
+
fontWeight: 650,
|
|
6661
|
+
color: "#2e9e5b"
|
|
6662
|
+
};
|
|
6663
|
+
const LIST_STYLE$2 = {
|
|
6664
|
+
listStyle: "none",
|
|
6665
|
+
margin: 0,
|
|
6666
|
+
padding: 0,
|
|
6667
|
+
maxHeight: "50vh",
|
|
6668
|
+
overflowY: "auto"
|
|
6669
|
+
};
|
|
6670
|
+
const ROW_STYLE$2 = {
|
|
6671
|
+
display: "flex",
|
|
6672
|
+
alignItems: "flex-start",
|
|
6673
|
+
gap: 8,
|
|
6674
|
+
padding: "7px 0",
|
|
6675
|
+
borderTop: "1px solid rgba(127,127,127,0.18)"
|
|
6676
|
+
};
|
|
6677
|
+
const INDEX_STYLE = {
|
|
6678
|
+
flexShrink: 0,
|
|
6679
|
+
minWidth: 18,
|
|
6680
|
+
fontSize: 11,
|
|
6681
|
+
lineHeight: "18px",
|
|
6682
|
+
opacity: .5,
|
|
6683
|
+
textAlign: "right"
|
|
6684
|
+
};
|
|
6685
|
+
const ROW_MAIN_STYLE$1 = {
|
|
6686
|
+
display: "flex",
|
|
6687
|
+
flexDirection: "column",
|
|
6688
|
+
gap: 2,
|
|
6689
|
+
flex: 1,
|
|
6690
|
+
minWidth: 0
|
|
5345
6691
|
};
|
|
5346
|
-
const
|
|
6692
|
+
const ROW_TITLE_STYLE$1 = {
|
|
6693
|
+
fontSize: 12.5,
|
|
6694
|
+
lineHeight: 1.45,
|
|
6695
|
+
fontWeight: 600,
|
|
6696
|
+
overflow: "hidden",
|
|
6697
|
+
display: "-webkit-box",
|
|
6698
|
+
WebkitLineClamp: 2,
|
|
6699
|
+
WebkitBoxOrient: "vertical",
|
|
6700
|
+
wordBreak: "break-word"
|
|
6701
|
+
};
|
|
6702
|
+
const ROW_ERROR_STYLE$1 = {
|
|
6703
|
+
fontSize: 11,
|
|
6704
|
+
lineHeight: 1.4,
|
|
6705
|
+
color: ERROR,
|
|
6706
|
+
opacity: .85,
|
|
6707
|
+
overflow: "hidden",
|
|
6708
|
+
display: "-webkit-box",
|
|
6709
|
+
WebkitLineClamp: 2,
|
|
6710
|
+
WebkitBoxOrient: "vertical",
|
|
6711
|
+
wordBreak: "break-word"
|
|
6712
|
+
};
|
|
6713
|
+
const STATUS_CHIP_STYLE = {
|
|
6714
|
+
display: "inline-flex",
|
|
6715
|
+
alignItems: "center",
|
|
6716
|
+
gap: 3,
|
|
6717
|
+
flexShrink: 0,
|
|
6718
|
+
padding: "1px 6px",
|
|
6719
|
+
borderRadius: 4,
|
|
6720
|
+
border: "1px solid currentColor",
|
|
6721
|
+
fontSize: 11,
|
|
6722
|
+
lineHeight: 1.5,
|
|
6723
|
+
whiteSpace: "nowrap"
|
|
6724
|
+
};
|
|
6725
|
+
const FOOTER_ACTIONS_STYLE = {
|
|
5347
6726
|
display: "flex",
|
|
5348
6727
|
alignItems: "center",
|
|
5349
|
-
|
|
5350
|
-
gap: 8
|
|
6728
|
+
gap: 6
|
|
5351
6729
|
};
|
|
5352
|
-
const
|
|
6730
|
+
const MODEL_BADGE_STYLE = {
|
|
5353
6731
|
display: "flex",
|
|
6732
|
+
flexWrap: "wrap",
|
|
5354
6733
|
alignItems: "center",
|
|
5355
|
-
gap:
|
|
6734
|
+
gap: 8,
|
|
6735
|
+
fontSize: 12,
|
|
6736
|
+
lineHeight: 1.5,
|
|
6737
|
+
paddingBottom: 4,
|
|
6738
|
+
borderBottom: "1px solid rgba(127,127,127,0.18)"
|
|
5356
6739
|
};
|
|
5357
|
-
const
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
6740
|
+
const MODEL_TAG_STYLE = {
|
|
6741
|
+
flexShrink: 0,
|
|
6742
|
+
padding: "1px 6px",
|
|
6743
|
+
borderRadius: 4,
|
|
6744
|
+
border: `1px solid ${ACCENT}`,
|
|
6745
|
+
color: ACCENT,
|
|
6746
|
+
fontWeight: 650,
|
|
6747
|
+
fontSize: 11
|
|
5361
6748
|
};
|
|
5362
|
-
const
|
|
5363
|
-
|
|
5364
|
-
|
|
6749
|
+
const REASON_STYLE = {
|
|
6750
|
+
opacity: .75,
|
|
6751
|
+
overflow: "hidden",
|
|
6752
|
+
textOverflow: "ellipsis",
|
|
6753
|
+
whiteSpace: "nowrap",
|
|
6754
|
+
maxWidth: "100%"
|
|
5365
6755
|
};
|
|
5366
|
-
const
|
|
5367
|
-
fontSize: 11
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
6756
|
+
const ROW_META_STYLE$1 = {
|
|
6757
|
+
fontSize: 11,
|
|
6758
|
+
lineHeight: 1.4,
|
|
6759
|
+
opacity: .65,
|
|
6760
|
+
overflow: "hidden",
|
|
6761
|
+
textOverflow: "ellipsis",
|
|
6762
|
+
whiteSpace: "nowrap"
|
|
5373
6763
|
};
|
|
5374
|
-
const
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
fontSize: 12
|
|
6764
|
+
const CANCELLED_STYLE = {
|
|
6765
|
+
fontSize: 12,
|
|
6766
|
+
fontWeight: 650,
|
|
6767
|
+
color: ERROR
|
|
5379
6768
|
};
|
|
5380
6769
|
// src/client/FullTextTab.tsx
|
|
5381
6770
|
/**
|
|
@@ -5405,7 +6794,7 @@ const EMPTY_LIST = {
|
|
|
5405
6794
|
function viewQuery(view) {
|
|
5406
6795
|
return view === "missing" ? { missingFullText: true } : { pdfStatus: view };
|
|
5407
6796
|
}
|
|
5408
|
-
function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBatchDownload, onBatchZotero, onShowInResults }) {
|
|
6797
|
+
function FullTextTab({ t, taskId, rev, states, stats, onDownload, onOpenLocal, onRevealLocal, onZotero, onBatchDownload, onBatchZotero, onShowInResults }) {
|
|
5409
6798
|
const [view, setView] = (0, react.useState)("missing");
|
|
5410
6799
|
const [list, setList] = (0, react.useState)(EMPTY_LIST);
|
|
5411
6800
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
@@ -5484,7 +6873,7 @@ function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBa
|
|
|
5484
6873
|
});
|
|
5485
6874
|
const coverage = stats?.fullText;
|
|
5486
6875
|
const availableCount = coverage === void 0 ? 0 : coverage.available + coverage.downloaded + coverage.htmlOnly;
|
|
5487
|
-
const missingCount = coverage === void 0 ? 0 : coverage.unknown + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
6876
|
+
const missingCount = coverage === void 0 ? 0 : coverage.unknown + coverage.missing + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
5488
6877
|
const selectedPapers = list.papers.filter((paper) => selected.has(paper.paperId));
|
|
5489
6878
|
const viewLabel = view === "missing" ? t("fulltext.missing") : t(`pdf.status.${view}`);
|
|
5490
6879
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5555,15 +6944,18 @@ function FullTextTab({ t, taskId, rev, states, stats, onDownload, onZotero, onBa
|
|
|
5555
6944
|
style: TEXT_STYLE$1,
|
|
5556
6945
|
children: t("fulltext.none")
|
|
5557
6946
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5558
|
-
style: LIST_STYLE$
|
|
6947
|
+
style: LIST_STYLE$1,
|
|
5559
6948
|
children: list.papers.map((paper) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FullTextRow, {
|
|
5560
6949
|
t,
|
|
5561
6950
|
paper,
|
|
5562
6951
|
selected: selected.has(paper.paperId),
|
|
5563
6952
|
status: paperStatus(states, paper),
|
|
5564
6953
|
error: states[paper.paperId]?.pdfError,
|
|
6954
|
+
downloadedPath: states[paper.paperId]?.pdfFile,
|
|
5565
6955
|
onToggleSelect: toggleSelect,
|
|
5566
6956
|
onDownload,
|
|
6957
|
+
...onOpenLocal === void 0 ? {} : { onOpenLocal: (path) => onOpenLocal(taskId, paper.paperId, path) },
|
|
6958
|
+
...onRevealLocal === void 0 ? {} : { onRevealLocal },
|
|
5567
6959
|
onZotero
|
|
5568
6960
|
}, paper.paperId))
|
|
5569
6961
|
}),
|
|
@@ -5615,7 +7007,9 @@ function BulkBar$1({ t, count, onSelectPage, onClear, onDownload, onZotero }) {
|
|
|
5615
7007
|
]
|
|
5616
7008
|
});
|
|
5617
7009
|
}
|
|
5618
|
-
function FullTextRow({ t, paper, selected, status, error, onToggleSelect, onDownload, onZotero }) {
|
|
7010
|
+
function FullTextRow({ t, paper, selected, status, error, downloadedPath, onToggleSelect, onDownload, onOpenLocal, onRevealLocal, onZotero }) {
|
|
7011
|
+
const downloaded = status === "downloaded" && downloadedPath !== void 0;
|
|
7012
|
+
const pdfMissing = status === "missing";
|
|
5619
7013
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
5620
7014
|
style: ROW_STYLE$1,
|
|
5621
7015
|
children: [
|
|
@@ -5659,11 +7053,18 @@ function FullTextRow({ t, paper, selected, status, error, onToggleSelect, onDown
|
|
|
5659
7053
|
}),
|
|
5660
7054
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5661
7055
|
style: ROW_ACTIONS_STYLE,
|
|
5662
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5663
|
-
|
|
7056
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
|
|
7057
|
+
t,
|
|
7058
|
+
label: downloaded ? t("pdf.downloadedAction") : pdfMissing ? t("pdf.missingAction") : t("pdf.download"),
|
|
7059
|
+
...downloaded ? {
|
|
7060
|
+
downloadedPath,
|
|
7061
|
+
hint: tpl(t("pdf.downloadedHint"), { path: downloadedPath ?? "" })
|
|
7062
|
+
} : pdfMissing ? { hint: t("pdf.missingHint") } : {},
|
|
7063
|
+
missing: pdfMissing,
|
|
5664
7064
|
style: LINK_STYLE,
|
|
5665
|
-
|
|
5666
|
-
|
|
7065
|
+
onDownload: () => onDownload(paper),
|
|
7066
|
+
...onOpenLocal === void 0 ? {} : { onOpen: onOpenLocal },
|
|
7067
|
+
...onRevealLocal === void 0 ? {} : { onReveal: onRevealLocal }
|
|
5667
7068
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5668
7069
|
type: "button",
|
|
5669
7070
|
style: LINK_STYLE,
|
|
@@ -5754,7 +7155,7 @@ const MORE_STYLE = {
|
|
|
5754
7155
|
fontSize: 11,
|
|
5755
7156
|
cursor: "pointer"
|
|
5756
7157
|
};
|
|
5757
|
-
const LIST_STYLE$
|
|
7158
|
+
const LIST_STYLE$1 = {
|
|
5758
7159
|
margin: 0,
|
|
5759
7160
|
padding: 0,
|
|
5760
7161
|
listStyle: "none"
|
|
@@ -5802,19 +7203,18 @@ const ROW_ACTIONS_STYLE = {
|
|
|
5802
7203
|
/**
|
|
5803
7204
|
* 任务「分组」tab:维护「为什么读」的分类,并把文献归入其中。
|
|
5804
7205
|
*
|
|
5805
|
-
*
|
|
7206
|
+
* 分组是任务内概念;归组在结果卡片的分组下拉里完成,这里负责增删改名、排序。
|
|
5806
7207
|
* 增删与排序立即保存,名称和说明在输入框失焦时保存——切到结果页或别处不会丢改动。
|
|
5807
7208
|
*/
|
|
5808
7209
|
function draftOf(groups) {
|
|
5809
7210
|
return groups.map((group) => ({ ...group }));
|
|
5810
7211
|
}
|
|
5811
|
-
function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
7212
|
+
function GroupsTab({ t, taskId, detail, onSaved, onPickGroup, onNotify }) {
|
|
5812
7213
|
const [draft, setDraft] = (0, react.useState)(() => draftOf(detail.groups));
|
|
5813
7214
|
const [name, setName] = (0, react.useState)("");
|
|
5814
7215
|
const [description, setDescription] = (0, react.useState)("");
|
|
5815
7216
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
5816
7217
|
const [error, setError] = (0, react.useState)(null);
|
|
5817
|
-
const [toast, setToast] = (0, react.useState)("");
|
|
5818
7218
|
const latest = (0, react.useRef)(draft);
|
|
5819
7219
|
const queued = (0, react.useRef)(false);
|
|
5820
7220
|
const saving = (0, react.useRef)(false);
|
|
@@ -5836,14 +7236,13 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5836
7236
|
saving.current = true;
|
|
5837
7237
|
setBusy(true);
|
|
5838
7238
|
setError(null);
|
|
5839
|
-
setToast("");
|
|
5840
7239
|
(async () => {
|
|
5841
7240
|
try {
|
|
5842
7241
|
while (queued.current) {
|
|
5843
7242
|
queued.current = false;
|
|
5844
7243
|
await updateTask(taskId, { groups: latest.current.map((group) => ({ ...group })) });
|
|
5845
7244
|
}
|
|
5846
|
-
|
|
7245
|
+
onNotify(t("groups.saved"));
|
|
5847
7246
|
onSaved();
|
|
5848
7247
|
} catch (reason) {
|
|
5849
7248
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
@@ -5855,7 +7254,8 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5855
7254
|
}, [
|
|
5856
7255
|
taskId,
|
|
5857
7256
|
t,
|
|
5858
|
-
onSaved
|
|
7257
|
+
onSaved,
|
|
7258
|
+
onNotify
|
|
5859
7259
|
]);
|
|
5860
7260
|
const add = () => {
|
|
5861
7261
|
const trimmed = name.trim();
|
|
@@ -5908,7 +7308,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5908
7308
|
style: LABEL_STYLE,
|
|
5909
7309
|
children: t("groups.name")
|
|
5910
7310
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5911
|
-
style: INPUT_STYLE$
|
|
7311
|
+
style: INPUT_STYLE$1,
|
|
5912
7312
|
value: name,
|
|
5913
7313
|
placeholder: t("groups.namePlaceholder"),
|
|
5914
7314
|
"aria-label": t("groups.name"),
|
|
@@ -5927,7 +7327,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5927
7327
|
style: LABEL_STYLE,
|
|
5928
7328
|
children: t("groups.desc")
|
|
5929
7329
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5930
|
-
style: INPUT_STYLE$
|
|
7330
|
+
style: INPUT_STYLE$1,
|
|
5931
7331
|
value: description,
|
|
5932
7332
|
placeholder: t("groups.descPlaceholder"),
|
|
5933
7333
|
"aria-label": t("groups.desc"),
|
|
@@ -5953,7 +7353,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5953
7353
|
style: statusStyle,
|
|
5954
7354
|
children: t("groups.empty")
|
|
5955
7355
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5956
|
-
style: LIST_STYLE
|
|
7356
|
+
style: LIST_STYLE,
|
|
5957
7357
|
children: draft.map((group, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5958
7358
|
style: GROUP_ROW_STYLE,
|
|
5959
7359
|
children: [
|
|
@@ -5961,7 +7361,7 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
5961
7361
|
style: GROUP_HEAD_STYLE,
|
|
5962
7362
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5963
7363
|
style: {
|
|
5964
|
-
...INPUT_STYLE$
|
|
7364
|
+
...INPUT_STYLE$1,
|
|
5965
7365
|
flex: 1,
|
|
5966
7366
|
fontWeight: 600
|
|
5967
7367
|
},
|
|
@@ -6024,15 +7424,12 @@ function GroupsTab({ t, taskId, detail, onSaved, onPickGroup }) {
|
|
|
6024
7424
|
]
|
|
6025
7425
|
}, group.id))
|
|
6026
7426
|
}),
|
|
6027
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
7427
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6028
7428
|
style: ACTION_ROW_STYLE$1,
|
|
6029
|
-
children:
|
|
6030
|
-
style: TOAST_STYLE$1,
|
|
6031
|
-
children: toast
|
|
6032
|
-
}), busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7429
|
+
children: busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6033
7430
|
style: statusStyle,
|
|
6034
7431
|
children: t("groups.saving")
|
|
6035
|
-
})
|
|
7432
|
+
})
|
|
6036
7433
|
}),
|
|
6037
7434
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6038
7435
|
style: {
|
|
@@ -6071,7 +7468,7 @@ const LABEL_STYLE = {
|
|
|
6071
7468
|
opacity: .7,
|
|
6072
7469
|
whiteSpace: "nowrap"
|
|
6073
7470
|
};
|
|
6074
|
-
const INPUT_STYLE$
|
|
7471
|
+
const INPUT_STYLE$1 = {
|
|
6075
7472
|
fontSize: 12,
|
|
6076
7473
|
padding: "3px 5px",
|
|
6077
7474
|
borderRadius: 4,
|
|
@@ -6081,13 +7478,13 @@ const INPUT_STYLE$2 = {
|
|
|
6081
7478
|
minWidth: 0,
|
|
6082
7479
|
flex: "1 1 120px"
|
|
6083
7480
|
};
|
|
6084
|
-
const LIST_STYLE
|
|
7481
|
+
const LIST_STYLE = {
|
|
6085
7482
|
display: "flex",
|
|
6086
7483
|
flexDirection: "column",
|
|
6087
7484
|
gap: 6
|
|
6088
7485
|
};
|
|
6089
7486
|
const ROW_INPUT_STYLE = {
|
|
6090
|
-
...INPUT_STYLE$
|
|
7487
|
+
...INPUT_STYLE$1,
|
|
6091
7488
|
flex: "0 0 auto"
|
|
6092
7489
|
};
|
|
6093
7490
|
const GROUP_ROW_STYLE = {
|
|
@@ -6113,55 +7510,25 @@ const ACTION_ROW_STYLE$1 = {
|
|
|
6113
7510
|
gap: 10,
|
|
6114
7511
|
flexWrap: "wrap"
|
|
6115
7512
|
};
|
|
6116
|
-
const TOAST_STYLE$1 = {
|
|
6117
|
-
fontSize: 12,
|
|
6118
|
-
color: "#2e9e5b"
|
|
6119
|
-
};
|
|
6120
7513
|
// src/client/NotesTab.tsx
|
|
6121
7514
|
/**
|
|
6122
|
-
* 任务「笔记」tab
|
|
7515
|
+
* 任务「笔记」tab:任务级研究笔记。
|
|
6123
7516
|
*
|
|
6124
|
-
*
|
|
6125
|
-
* `updateTask({notes})`,与模型工具看到的是同一份数据。
|
|
7517
|
+
* 任务笔记直接写回 `updateTask({notes})`,与模型工具看到的是同一份数据。
|
|
6126
7518
|
*/
|
|
6127
|
-
|
|
6128
|
-
function NotesTab({ t, taskId, detail, onSaved }) {
|
|
7519
|
+
function NotesTab({ t, taskId, detail, onSaved, onNotify }) {
|
|
6129
7520
|
const [notes, setNotes] = (0, react.useState)(detail.notes);
|
|
6130
7521
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
6131
|
-
const [toast, setToast] = (0, react.useState)("");
|
|
6132
7522
|
const [error, setError] = (0, react.useState)(null);
|
|
6133
|
-
const [titles, setTitles] = (0, react.useState)({});
|
|
6134
|
-
const [loadingTitles, setLoadingTitles] = (0, react.useState)(true);
|
|
6135
7523
|
(0, react.useEffect)(() => {
|
|
6136
7524
|
setNotes(detail.notes);
|
|
6137
7525
|
}, [detail.rev]);
|
|
6138
|
-
(0, react.useEffect)(() => {
|
|
6139
|
-
const controller = new AbortController();
|
|
6140
|
-
setLoadingTitles(true);
|
|
6141
|
-
fetchResults(taskId, { limit: NOTE_PAGE_LIMIT }, controller.signal).then((page) => {
|
|
6142
|
-
if (controller.signal.aborted) return;
|
|
6143
|
-
const map = {};
|
|
6144
|
-
for (const paper of page.papers) map[paper.paperId] = paper.title;
|
|
6145
|
-
setTitles(map);
|
|
6146
|
-
}).catch(() => void 0).finally(() => {
|
|
6147
|
-
if (!controller.signal.aborted) setLoadingTitles(false);
|
|
6148
|
-
});
|
|
6149
|
-
return () => {
|
|
6150
|
-
controller.abort();
|
|
6151
|
-
};
|
|
6152
|
-
}, [taskId, detail.rev]);
|
|
6153
|
-
const entries = (0, react.useMemo)(() => {
|
|
6154
|
-
return Object.entries(detail.states).filter(([, state]) => (state.note ?? "") !== "" || (state.tags ?? []).length > 0).map(([paperId, state]) => ({
|
|
6155
|
-
paperId,
|
|
6156
|
-
state
|
|
6157
|
-
}));
|
|
6158
|
-
}, [detail.states]);
|
|
6159
7526
|
const save = () => {
|
|
6160
7527
|
if (busy) return;
|
|
6161
7528
|
setBusy(true);
|
|
6162
7529
|
setError(null);
|
|
6163
7530
|
updateTask(taskId, { notes }).then(() => {
|
|
6164
|
-
|
|
7531
|
+
onNotify(t("notes.saved"));
|
|
6165
7532
|
onSaved();
|
|
6166
7533
|
}).catch((reason) => {
|
|
6167
7534
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
@@ -6169,9 +7536,9 @@ function NotesTab({ t, taskId, detail, onSaved }) {
|
|
|
6169
7536
|
setBusy(false);
|
|
6170
7537
|
});
|
|
6171
7538
|
};
|
|
6172
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7539
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6173
7540
|
style: WRAP_STYLE$2,
|
|
6174
|
-
children:
|
|
7541
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
6175
7542
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
6176
7543
|
style: HEAD_STYLE,
|
|
6177
7544
|
children: t("notes.title")
|
|
@@ -6183,19 +7550,16 @@ function NotesTab({ t, taskId, detail, onSaved }) {
|
|
|
6183
7550
|
placeholder: t("notes.taskPlaceholder"),
|
|
6184
7551
|
onChange: (event) => setNotes(event.target.value)
|
|
6185
7552
|
}),
|
|
6186
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
7553
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6187
7554
|
style: ACTION_ROW_STYLE,
|
|
6188
|
-
children:
|
|
7555
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
6189
7556
|
variant: "outline",
|
|
6190
7557
|
size: "sm",
|
|
6191
7558
|
type: "button",
|
|
6192
7559
|
disabled: busy || notes === detail.notes,
|
|
6193
7560
|
onClick: save,
|
|
6194
7561
|
children: t("notes.save")
|
|
6195
|
-
})
|
|
6196
|
-
style: TOAST_STYLE,
|
|
6197
|
-
children: toast
|
|
6198
|
-
})]
|
|
7562
|
+
})
|
|
6199
7563
|
}),
|
|
6200
7564
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6201
7565
|
style: {
|
|
@@ -6204,42 +7568,7 @@ function NotesTab({ t, taskId, detail, onSaved }) {
|
|
|
6204
7568
|
},
|
|
6205
7569
|
children: error
|
|
6206
7570
|
})
|
|
6207
|
-
] })
|
|
6208
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
6209
|
-
style: HEAD_STYLE,
|
|
6210
|
-
children: t("notes.paperSection")
|
|
6211
|
-
}),
|
|
6212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6213
|
-
style: statusStyle,
|
|
6214
|
-
children: t("notes.hint")
|
|
6215
|
-
}),
|
|
6216
|
-
loadingTitles && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6217
|
-
style: statusStyle,
|
|
6218
|
-
children: t("notes.loading")
|
|
6219
|
-
}) : entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6220
|
-
style: statusStyle,
|
|
6221
|
-
children: t("notes.empty")
|
|
6222
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6223
|
-
style: LIST_STYLE,
|
|
6224
|
-
children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6225
|
-
style: ENTRY_STYLE,
|
|
6226
|
-
children: [
|
|
6227
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6228
|
-
style: ENTRY_TITLE_STYLE,
|
|
6229
|
-
children: titles[entry.paperId] ?? entry.paperId
|
|
6230
|
-
}),
|
|
6231
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6232
|
-
style: ENTRY_META_STYLE,
|
|
6233
|
-
children: [t(`screening.${entry.state.screening}`), entry.state.tags === void 0 || entry.state.tags.length === 0 ? "" : ` · ${tpl(t("notes.tags"), { value: entry.state.tags.join(", ") })}`]
|
|
6234
|
-
}),
|
|
6235
|
-
entry.state.note !== void 0 && entry.state.note !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6236
|
-
style: ENTRY_NOTE_STYLE,
|
|
6237
|
-
children: entry.state.note
|
|
6238
|
-
})
|
|
6239
|
-
]
|
|
6240
|
-
}, entry.paperId))
|
|
6241
|
-
})
|
|
6242
|
-
] })]
|
|
7571
|
+
] })
|
|
6243
7572
|
});
|
|
6244
7573
|
}
|
|
6245
7574
|
const WRAP_STYLE$2 = {
|
|
@@ -6272,35 +7601,6 @@ const ACTION_ROW_STYLE = {
|
|
|
6272
7601
|
gap: 8,
|
|
6273
7602
|
marginTop: 6
|
|
6274
7603
|
};
|
|
6275
|
-
const LIST_STYLE = {
|
|
6276
|
-
display: "flex",
|
|
6277
|
-
flexDirection: "column"
|
|
6278
|
-
};
|
|
6279
|
-
const ENTRY_STYLE = {
|
|
6280
|
-
display: "flex",
|
|
6281
|
-
flexDirection: "column",
|
|
6282
|
-
gap: 2,
|
|
6283
|
-
padding: "6px 0",
|
|
6284
|
-
borderTop: "1px solid rgba(127,127,127,0.14)"
|
|
6285
|
-
};
|
|
6286
|
-
const ENTRY_TITLE_STYLE = {
|
|
6287
|
-
fontSize: 12.5,
|
|
6288
|
-
fontWeight: 650,
|
|
6289
|
-
lineHeight: 1.45
|
|
6290
|
-
};
|
|
6291
|
-
const ENTRY_META_STYLE = {
|
|
6292
|
-
fontSize: 11,
|
|
6293
|
-
opacity: .7
|
|
6294
|
-
};
|
|
6295
|
-
const ENTRY_NOTE_STYLE = {
|
|
6296
|
-
fontSize: 12,
|
|
6297
|
-
lineHeight: 1.55,
|
|
6298
|
-
opacity: .85
|
|
6299
|
-
};
|
|
6300
|
-
const TOAST_STYLE = {
|
|
6301
|
-
fontSize: 12,
|
|
6302
|
-
color: "#2e9e5b"
|
|
6303
|
-
};
|
|
6304
7604
|
// src/client/OverviewTab.tsx
|
|
6305
7605
|
/**
|
|
6306
7606
|
* 任务「概览」tab:一眼看清本轮检索发生了什么。
|
|
@@ -6349,6 +7649,11 @@ const FULLTEXT_STATUSES = [
|
|
|
6349
7649
|
key: "downloaded",
|
|
6350
7650
|
color: C.green
|
|
6351
7651
|
},
|
|
7652
|
+
{
|
|
7653
|
+
status: "missing",
|
|
7654
|
+
key: "missing",
|
|
7655
|
+
color: "#d97706"
|
|
7656
|
+
},
|
|
6352
7657
|
{
|
|
6353
7658
|
status: "html-only",
|
|
6354
7659
|
key: "htmlOnly",
|
|
@@ -7685,8 +8990,7 @@ const QUICK_FILTER_STYLE$1 = {
|
|
|
7685
8990
|
/**
|
|
7686
8991
|
* 任务结果里的单篇文献卡片。
|
|
7687
8992
|
*
|
|
7688
|
-
*
|
|
7689
|
-
* 备注 / 标签 / 必读顺序收在「标注」里,避免窄栏里堆满输入框。
|
|
8993
|
+
* 标题可点外链;徽章展示命中来源、被引、开放获取。筛读状态与分组就地修改。
|
|
7690
8994
|
*/
|
|
7691
8995
|
const ABSTRACT_PREVIEW = 260;
|
|
7692
8996
|
/** 分组下拉里「去新建分组」的哨兵值,不会与真实 group id 冲突。 */
|
|
@@ -7697,32 +9001,18 @@ const SCREENINGS$2 = [
|
|
|
7697
9001
|
"included",
|
|
7698
9002
|
"excluded"
|
|
7699
9003
|
];
|
|
9004
|
+
/** 出版商明确要求订阅 / 拒绝访问时,不再提供「下载 PDF」,避免必然失败的操作。 */
|
|
9005
|
+
const NO_PDF_STATUSES = /* @__PURE__ */ new Set(["paywalled", "blocked"]);
|
|
7700
9006
|
const SCREENING_COLOR = {
|
|
7701
9007
|
unread: "rgba(127,127,127,0.9)",
|
|
7702
9008
|
read: "#508cff",
|
|
7703
9009
|
included: "#2e9e5b",
|
|
7704
9010
|
excluded: "#d64545"
|
|
7705
9011
|
};
|
|
7706
|
-
function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, onToggleSelect, onInsert, onAnnotate, onDownload, onZotero, onOpenPaper, onManageGroups }) {
|
|
9012
|
+
function PaperCard({ paper, t, taskId, state, groups, busy, downloadDir, download, onOpenLocal, onRevealLocal, selected = false, onToggleSelect, onInsert, onAnnotate, onDownload, onZotero, onOpenPaper, onManageGroups }) {
|
|
7707
9013
|
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
7708
|
-
const [editing, setEditing] = (0, react.useState)(false);
|
|
7709
9014
|
const [entities, setEntities] = (0, react.useState)(false);
|
|
7710
|
-
const [
|
|
7711
|
-
const [note, setNote] = (0, react.useState)(state?.note ?? "");
|
|
7712
|
-
const [tags, setTags] = (0, react.useState)((state?.tags ?? []).join(", "));
|
|
7713
|
-
const [order, setOrder] = (0, react.useState)(state?.shortlistOrder === void 0 ? "" : String(state.shortlistOrder));
|
|
7714
|
-
const noteKey = state?.note ?? "";
|
|
7715
|
-
const tagsKey = (state?.tags ?? []).join(",");
|
|
7716
|
-
const orderKey = state?.shortlistOrder ?? "";
|
|
7717
|
-
(0, react.useEffect)(() => {
|
|
7718
|
-
setNote(noteKey);
|
|
7719
|
-
setTags(tagsKey);
|
|
7720
|
-
setOrder(orderKey === "" ? "" : String(orderKey));
|
|
7721
|
-
}, [
|
|
7722
|
-
noteKey,
|
|
7723
|
-
tagsKey,
|
|
7724
|
-
orderKey
|
|
7725
|
-
]);
|
|
9015
|
+
const [showHtml, setShowHtml] = (0, react.useState)(false);
|
|
7726
9016
|
const meta = [
|
|
7727
9017
|
paper.authors.length === 0 ? "" : `${paper.authors.slice(0, 3).join(", ")}${paper.authors.length > 3 ? " et al." : ""}`,
|
|
7728
9018
|
paper.year === null ? "" : String(paper.year),
|
|
@@ -7733,22 +9023,19 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7733
9023
|
const abstract = expanded || !canCollapse ? paper.abstract : `${paper.abstract.slice(0, ABSTRACT_PREVIEW)}…`;
|
|
7734
9024
|
const screening = state?.screening ?? "unread";
|
|
7735
9025
|
const groupName = groups.find((group) => group.id === state?.groupId)?.name;
|
|
7736
|
-
const hasNote = (state?.note ?? "") !== "" || (state?.tags ?? []).length > 0;
|
|
7737
9026
|
const pdfStatus = state?.pdfStatus ?? (paper.pdfUrl === "" ? "unknown" : "available");
|
|
7738
|
-
const canPdf = paper.pdfUrl !== "" || paper.ids.pmcid !== "" || paper.ids.arxivId !== "" || paper.doi !== "";
|
|
9027
|
+
const canPdf = !NO_PDF_STATUSES.has(pdfStatus) && (paper.pdfUrl !== "" || paper.ids.pmcid !== "" || paper.ids.arxivId !== "" || paper.doi !== "");
|
|
7739
9028
|
const canHtml = paper.ids.pmcid !== "";
|
|
7740
|
-
const
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
setEditing(false);
|
|
7751
|
-
};
|
|
9029
|
+
const downloading = download?.state === "downloading";
|
|
9030
|
+
const pdfMissing = !downloading && pdfStatus === "missing";
|
|
9031
|
+
const downloaded = !pdfMissing && (download?.state === "downloaded" || pdfStatus === "downloaded" && !downloading);
|
|
9032
|
+
const downloadedPath = downloaded ? download?.path ?? state?.pdfFile : void 0;
|
|
9033
|
+
const failedMessage = download?.state === "failed" ? download.error : pdfStatus === "failed" ? state?.pdfError : void 0;
|
|
9034
|
+
const pdfLabel = downloading ? t("pdf.downloading") : downloaded ? t("pdf.downloadedAction") : pdfMissing ? t("pdf.missingAction") : pdfStatus === "failed" ? t("pdf.retry") : t("pdf.download");
|
|
9035
|
+
const pdfHint = downloading ? t("pdf.downloadingHint") : downloaded ? tpl(t("pdf.downloadedHint"), { path: downloadedPath ?? downloadDir ?? "" }) : pdfMissing ? t("pdf.missingHint") : failedMessage !== void 0 ? tpl(t("pdf.retryHint"), { message: failedMessage }) : tpl(t("pdf.downloadHint"), { path: downloadDir ?? t("pdf.downloadDirUnknown") });
|
|
9036
|
+
const zoteroLabel = state?.zoteroStatus === "synced" ? t("pdf.zoteroSynced") : state?.zoteroStatus === "failed" ? t("pdf.zoteroRetry") : t("pdf.zotero");
|
|
9037
|
+
const zoteroHint = state?.zoteroStatus === "synced" ? state.zoteroCollection : state?.zoteroStatus === "failed" ? state.zoteroError : void 0;
|
|
9038
|
+
const zoteroColor = state?.zoteroStatus === "synced" ? "#2e9e5b" : state?.zoteroStatus === "failed" ? ERROR : void 0;
|
|
7752
9039
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
7753
9040
|
style: cardStyle,
|
|
7754
9041
|
children: [
|
|
@@ -7807,63 +9094,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7807
9094
|
children: t("annotate.createGroup")
|
|
7808
9095
|
})
|
|
7809
9096
|
]
|
|
7810
|
-
}),
|
|
7811
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7812
|
-
type: "button",
|
|
7813
|
-
style: linkButtonStyle,
|
|
7814
|
-
onClick: () => setEditing((value) => !value),
|
|
7815
|
-
children: [editing ? t("annotate.collapse") : t("annotate.expand"), hasNote ? " •" : ""]
|
|
7816
|
-
})
|
|
7817
|
-
]
|
|
7818
|
-
}),
|
|
7819
|
-
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7820
|
-
style: EDIT_STYLE,
|
|
7821
|
-
children: [
|
|
7822
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7823
|
-
style: EDIT_FIELD_STYLE,
|
|
7824
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.note") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
7825
|
-
style: NOTE_STYLE,
|
|
7826
|
-
value: note,
|
|
7827
|
-
rows: 2,
|
|
7828
|
-
placeholder: t("annotate.notePlaceholder"),
|
|
7829
|
-
onChange: (event) => setNote(event.target.value)
|
|
7830
|
-
})]
|
|
7831
|
-
}),
|
|
7832
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7833
|
-
style: EDIT_FIELD_STYLE,
|
|
7834
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.tags") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7835
|
-
style: INPUT_STYLE$1,
|
|
7836
|
-
value: tags,
|
|
7837
|
-
placeholder: t("annotate.tagsPlaceholder"),
|
|
7838
|
-
onChange: (event) => setTags(event.target.value)
|
|
7839
|
-
})]
|
|
7840
|
-
}),
|
|
7841
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7842
|
-
style: {
|
|
7843
|
-
...EDIT_FIELD_STYLE,
|
|
7844
|
-
maxWidth: 120
|
|
7845
|
-
},
|
|
7846
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("annotate.shortlistOrder") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7847
|
-
style: INPUT_STYLE$1,
|
|
7848
|
-
inputMode: "numeric",
|
|
7849
|
-
value: order,
|
|
7850
|
-
placeholder: "—",
|
|
7851
|
-
onChange: (event) => setOrder(event.target.value)
|
|
7852
|
-
})]
|
|
7853
|
-
}),
|
|
7854
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7855
|
-
style: {
|
|
7856
|
-
display: "flex",
|
|
7857
|
-
justifyContent: "flex-end"
|
|
7858
|
-
},
|
|
7859
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
7860
|
-
variant: "outline",
|
|
7861
|
-
size: "sm",
|
|
7862
|
-
type: "button",
|
|
7863
|
-
disabled: busy,
|
|
7864
|
-
onClick: saveDetail,
|
|
7865
|
-
children: t("annotate.save")
|
|
7866
|
-
})
|
|
7867
9097
|
})
|
|
7868
9098
|
]
|
|
7869
9099
|
}),
|
|
@@ -7931,10 +9161,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7931
9161
|
})] })]
|
|
7932
9162
|
})]
|
|
7933
9163
|
}),
|
|
7934
|
-
state?.pdfError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7935
|
-
style: PDF_ERROR_STYLE,
|
|
7936
|
-
children: tpl(t("pdf.error"), { message: state.pdfError })
|
|
7937
|
-
}),
|
|
7938
9164
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7939
9165
|
style: CARD_ROW_STYLE,
|
|
7940
9166
|
title: t("result.infoHint"),
|
|
@@ -7959,10 +9185,6 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7959
9185
|
style: INFO_BADGE_STYLE,
|
|
7960
9186
|
children: groupName
|
|
7961
9187
|
}),
|
|
7962
|
-
state?.shortlistOrder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7963
|
-
style: INFO_BADGE_STYLE,
|
|
7964
|
-
children: tpl(t("annotate.shortlistBadge"), { n: state.shortlistOrder })
|
|
7965
|
-
}),
|
|
7966
9188
|
sources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7967
9189
|
style: INFO_BADGE_STYLE,
|
|
7968
9190
|
children: t(`source.${source}`)
|
|
@@ -7980,6 +9202,7 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
7980
9202
|
...INFO_BADGE_STYLE,
|
|
7981
9203
|
color: PDF_COLOR[pdfStatus]
|
|
7982
9204
|
},
|
|
9205
|
+
title: state?.pdfError,
|
|
7983
9206
|
children: t(`pdf.status.${pdfStatus}`)
|
|
7984
9207
|
})
|
|
7985
9208
|
]
|
|
@@ -8020,31 +9243,35 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
8020
9243
|
onClick: () => setEntities((value) => !value),
|
|
8021
9244
|
children: t("entity.title")
|
|
8022
9245
|
}),
|
|
8023
|
-
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
type: "button",
|
|
8031
|
-
style: linkButtonStyle,
|
|
9246
|
+
canPdf && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
|
|
9247
|
+
t,
|
|
9248
|
+
label: pdfLabel,
|
|
9249
|
+
hint: pdfHint,
|
|
9250
|
+
...downloadedPath === void 0 ? {} : { downloadedPath },
|
|
9251
|
+
missing: pdfMissing,
|
|
9252
|
+
downloading,
|
|
8032
9253
|
disabled: busy,
|
|
8033
|
-
|
|
8034
|
-
|
|
9254
|
+
style: linkButtonStyle,
|
|
9255
|
+
onDownload: () => onDownload(paper),
|
|
9256
|
+
...onOpenLocal === void 0 ? {} : { onOpen: (path) => onOpenLocal(taskId, paper.paperId, path) },
|
|
9257
|
+
...onRevealLocal === void 0 ? {} : { onReveal: onRevealLocal }
|
|
8035
9258
|
}),
|
|
8036
9259
|
canHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8037
9260
|
type: "button",
|
|
8038
9261
|
style: linkButtonStyle,
|
|
8039
|
-
onClick: () =>
|
|
9262
|
+
onClick: () => setShowHtml((value) => !value),
|
|
8040
9263
|
children: t("pdf.fulltext")
|
|
8041
9264
|
}),
|
|
8042
9265
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8043
9266
|
type: "button",
|
|
8044
|
-
style: linkButtonStyle
|
|
9267
|
+
style: zoteroColor === void 0 ? linkButtonStyle : {
|
|
9268
|
+
...linkButtonStyle,
|
|
9269
|
+
color: zoteroColor
|
|
9270
|
+
},
|
|
8045
9271
|
disabled: busy,
|
|
9272
|
+
title: zoteroHint,
|
|
8046
9273
|
onClick: () => onZotero(paper),
|
|
8047
|
-
children:
|
|
9274
|
+
children: zoteroLabel
|
|
8048
9275
|
}),
|
|
8049
9276
|
paper.pdfUrl !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
8050
9277
|
style: linkStyle,
|
|
@@ -8069,21 +9296,11 @@ function PaperCard({ paper, t, taskId, state, groups, busy, selected = false, on
|
|
|
8069
9296
|
paperId: paper.paperId,
|
|
8070
9297
|
pmid: paper.ids.pmid
|
|
8071
9298
|
}),
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
type: "button",
|
|
8078
|
-
style: linkButtonStyle,
|
|
8079
|
-
onClick: () => setPreview(null),
|
|
8080
|
-
children: t("pdf.closePreview")
|
|
8081
|
-
})]
|
|
8082
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
8083
|
-
title: preview === "pdf" ? t("pdf.preview") : t("pdf.fulltext"),
|
|
8084
|
-
src: preview === "pdf" ? paperPdfUrl(taskId, paper.paperId) : paperFullTextUrl(taskId, paper.paperId),
|
|
8085
|
-
style: IFRAME_STYLE
|
|
8086
|
-
})]
|
|
9299
|
+
showHtml && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreviewFrame, {
|
|
9300
|
+
t,
|
|
9301
|
+
src: paperFullTextUrl(taskId, paper.paperId),
|
|
9302
|
+
height: 360,
|
|
9303
|
+
onClose: () => setShowHtml(false)
|
|
8087
9304
|
})
|
|
8088
9305
|
]
|
|
8089
9306
|
});
|
|
@@ -8092,39 +9309,13 @@ const PDF_COLOR = {
|
|
|
8092
9309
|
unknown: "rgba(127,127,127,0.8)",
|
|
8093
9310
|
available: "#508cff",
|
|
8094
9311
|
downloaded: "#2e9e5b",
|
|
9312
|
+
missing: "#d64545",
|
|
8095
9313
|
"html-only": "#b26a00",
|
|
8096
9314
|
paywalled: "#b26a00",
|
|
8097
9315
|
blocked: "#d64545",
|
|
8098
9316
|
failed: "#d64545"
|
|
8099
9317
|
};
|
|
8100
|
-
|
|
8101
|
-
fontSize: 11,
|
|
8102
|
-
lineHeight: 1.5,
|
|
8103
|
-
color: "#b26a00",
|
|
8104
|
-
marginTop: 2
|
|
8105
|
-
};
|
|
8106
|
-
const PREVIEW_WRAP_STYLE = {
|
|
8107
|
-
marginTop: 6,
|
|
8108
|
-
border: "1px solid rgba(127,127,127,0.22)",
|
|
8109
|
-
borderRadius: 6,
|
|
8110
|
-
overflow: "hidden"
|
|
8111
|
-
};
|
|
8112
|
-
const PREVIEW_HEAD_STYLE = {
|
|
8113
|
-
display: "flex",
|
|
8114
|
-
alignItems: "center",
|
|
8115
|
-
justifyContent: "space-between",
|
|
8116
|
-
gap: 8,
|
|
8117
|
-
padding: "3px 8px",
|
|
8118
|
-
fontSize: 11,
|
|
8119
|
-
background: "rgba(127,127,127,0.08)"
|
|
8120
|
-
};
|
|
8121
|
-
const IFRAME_STYLE = {
|
|
8122
|
-
width: "100%",
|
|
8123
|
-
height: 360,
|
|
8124
|
-
border: "none",
|
|
8125
|
-
background: "#fff"
|
|
8126
|
-
};
|
|
8127
|
-
/** 卡片顶部工具行:勾选 / 筛读 / 分组 / 标注。 */
|
|
9318
|
+
/** 卡片顶部工具行:勾选 / 筛读 / 分组。 */
|
|
8128
9319
|
const STATE_ROW_STYLE = {
|
|
8129
9320
|
display: "flex",
|
|
8130
9321
|
alignItems: "center",
|
|
@@ -8187,44 +9378,13 @@ const INFO_BADGE_STYLE = {
|
|
|
8187
9378
|
border: "1px solid rgba(18,131,108,0.45)"
|
|
8188
9379
|
};
|
|
8189
9380
|
const SELECT_STYLE = {
|
|
8190
|
-
fontSize: 11,
|
|
8191
|
-
padding: "2px 4px",
|
|
8192
|
-
borderRadius: 4,
|
|
8193
|
-
border: "1px solid rgba(127,127,127,0.28)",
|
|
8194
|
-
background: "transparent",
|
|
8195
|
-
color: "inherit",
|
|
8196
|
-
maxWidth: 130
|
|
8197
|
-
};
|
|
8198
|
-
const EDIT_STYLE = {
|
|
8199
|
-
display: "flex",
|
|
8200
|
-
flexDirection: "column",
|
|
8201
|
-
gap: 6,
|
|
8202
|
-
marginBottom: 8,
|
|
8203
|
-
padding: "6px 8px",
|
|
8204
|
-
borderRadius: 6,
|
|
8205
|
-
background: "rgba(127,127,127,0.07)"
|
|
8206
|
-
};
|
|
8207
|
-
const EDIT_FIELD_STYLE = {
|
|
8208
|
-
display: "flex",
|
|
8209
|
-
flexDirection: "column",
|
|
8210
|
-
gap: 2,
|
|
8211
|
-
fontSize: 11,
|
|
8212
|
-
opacity: .9
|
|
8213
|
-
};
|
|
8214
|
-
const INPUT_STYLE$1 = {
|
|
8215
|
-
fontSize: 12,
|
|
8216
|
-
padding: "3px 5px",
|
|
9381
|
+
fontSize: 11,
|
|
9382
|
+
padding: "2px 4px",
|
|
8217
9383
|
borderRadius: 4,
|
|
8218
9384
|
border: "1px solid rgba(127,127,127,0.28)",
|
|
8219
9385
|
background: "transparent",
|
|
8220
9386
|
color: "inherit",
|
|
8221
|
-
|
|
8222
|
-
};
|
|
8223
|
-
const NOTE_STYLE = {
|
|
8224
|
-
...INPUT_STYLE$1,
|
|
8225
|
-
resize: "vertical",
|
|
8226
|
-
fontFamily: "inherit",
|
|
8227
|
-
lineHeight: 1.5
|
|
9387
|
+
maxWidth: 130
|
|
8228
9388
|
};
|
|
8229
9389
|
// src/client/RoundsDialog.tsx
|
|
8230
9390
|
/**
|
|
@@ -9014,6 +10174,13 @@ const CHIP_REMOVE_STYLE = {
|
|
|
9014
10174
|
* 避免多个 tab 空转。重跑 / 取消都走同一套刷新。
|
|
9015
10175
|
*/
|
|
9016
10176
|
const POLL_MS = 1500;
|
|
10177
|
+
/**
|
|
10178
|
+
* 结果页每页篇数(与检索条件的「每源条数」无关,只影响面板展示)。
|
|
10179
|
+
*
|
|
10180
|
+
* 与 Host 侧「第 N 页」共用一个常量:模型说「第 2 页」时,宿主就是按这个
|
|
10181
|
+
* 粒度切片的;两处各写一份会静默错位。
|
|
10182
|
+
*/
|
|
10183
|
+
const RESULT_PAGE_SIZE = 20;
|
|
9017
10184
|
const INITIAL = {
|
|
9018
10185
|
detail: null,
|
|
9019
10186
|
page: null,
|
|
@@ -9026,7 +10193,7 @@ function isActive(status) {
|
|
|
9026
10193
|
function errorMessage(error) {
|
|
9027
10194
|
return error instanceof Error ? error.message : String(error);
|
|
9028
10195
|
}
|
|
9029
|
-
function useTask(id, signal, pageSize =
|
|
10196
|
+
function useTask(id, signal, pageSize = RESULT_PAGE_SIZE, filters = {}) {
|
|
9030
10197
|
const [state, setState] = (0, react.useState)(INITIAL);
|
|
9031
10198
|
const [nonce, setNonce] = (0, react.useState)(0);
|
|
9032
10199
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
@@ -9250,15 +10417,55 @@ function formatDate(value) {
|
|
|
9250
10417
|
const date = new Date(value);
|
|
9251
10418
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
9252
10419
|
}
|
|
9253
|
-
|
|
10420
|
+
/** `ResultsQuery` 的扁平投影:只保留已生效、可序列化的筛选值。 */
|
|
10421
|
+
function snapshotFilters(filters) {
|
|
10422
|
+
const out = {};
|
|
10423
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
10424
|
+
if (key === "offset" || key === "limit") continue;
|
|
10425
|
+
if (value === void 0 || value === null || value === "" || value === false) continue;
|
|
10426
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") out[key] = value;
|
|
10427
|
+
}
|
|
10428
|
+
return out;
|
|
10429
|
+
}
|
|
10430
|
+
function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onCloseTask, onOpenLocal, onRevealLocal }) {
|
|
9254
10431
|
const info = useTabInfo();
|
|
9255
10432
|
const taskId = parseTaskId(info);
|
|
9256
10433
|
const [filters, setFilters] = (0, react.useState)({});
|
|
9257
10434
|
const [tab, setTab] = (0, react.useState)("results");
|
|
9258
|
-
const view = useTask(taskId, info.tab.signal,
|
|
10435
|
+
const view = useTask(taskId, info.tab.signal, RESULT_PAGE_SIZE, filters);
|
|
9259
10436
|
const bodyRef = (0, react.useRef)(null);
|
|
9260
|
-
const [toast,
|
|
10437
|
+
const [toast, setToastState] = (0, react.useState)("");
|
|
10438
|
+
const [toastTone, setToastTone] = (0, react.useState)("info");
|
|
10439
|
+
const [toastMs, setToastMs] = (0, react.useState)(4e3);
|
|
10440
|
+
/** 统一的底部浮层提示:tone 区分普通结果与下载失败。 */
|
|
10441
|
+
const notify = (text, tone = "info", ms = 4e3) => {
|
|
10442
|
+
setToastState(text);
|
|
10443
|
+
setToastTone(tone);
|
|
10444
|
+
setToastMs(ms);
|
|
10445
|
+
};
|
|
10446
|
+
/** paperId → 下载状态;刷新任务后仍保留「下载中 / 已下载」的即时反馈。 */
|
|
10447
|
+
const [downloads, setDownloads] = (0, react.useState)({});
|
|
10448
|
+
/** 批量下载弹窗:只集中展示,真实状态仍写在 downloads / 任务详情的 states 里。 */
|
|
10449
|
+
const [batchItems, setBatchItems] = (0, react.useState)([]);
|
|
10450
|
+
const [batchOpen, setBatchOpen] = (0, react.useState)(false);
|
|
10451
|
+
/** Zotero 同步弹窗:单篇与批量共用,先选目标目录再同步,并展示逐篇结果。 */
|
|
10452
|
+
const [zoteroItems, setZoteroItems] = (0, react.useState)([]);
|
|
10453
|
+
const [zoteroOpen, setZoteroOpen] = (0, react.useState)(false);
|
|
10454
|
+
/** 模型发起的同步:范围说明、原话与目标目录;手动操作时为 undefined。 */
|
|
10455
|
+
const [zoteroMeta, setZoteroMeta] = (0, react.useState)(void 0);
|
|
10456
|
+
const [batchRunning, setBatchRunning] = (0, react.useState)(false);
|
|
10457
|
+
const batchRunningRef = (0, react.useRef)(false);
|
|
10458
|
+
/** 模型发起的下载:弹窗标明来源/范围,并提供独立的「取消下载」。 */
|
|
10459
|
+
const [batchMeta, setBatchMeta] = (0, react.useState)({ origin: "panel" });
|
|
10460
|
+
const [batchCancelled, setBatchCancelled] = (0, react.useState)(false);
|
|
10461
|
+
const batchAbortRef = (0, react.useRef)(null);
|
|
10462
|
+
/** 待消费的模型指令;tab 未挂载时由模块级 store 暂存,focus 拉起后再消费。 */
|
|
10463
|
+
const pendingCommands = usePendingDownloadCommands(taskId);
|
|
10464
|
+
const pendingZoteroCommands = usePendingZoteroCommands(taskId);
|
|
9261
10465
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
10466
|
+
/** 结果页里见过的文献:跨页选中的条目做「加入对话」时要能拿到论文详情。 */
|
|
10467
|
+
const papersByIdRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
10468
|
+
const ids = [...selected];
|
|
9262
10469
|
const [editingConditions, setEditingConditions] = (0, react.useState)(false);
|
|
9263
10470
|
const [risFallback, setRisFallback] = (0, react.useState)(null);
|
|
9264
10471
|
const [showRounds, setShowRounds] = (0, react.useState)(false);
|
|
@@ -9267,10 +10474,28 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9267
10474
|
const [overview, setOverview] = (0, react.useState)(null);
|
|
9268
10475
|
const detail = view.detail;
|
|
9269
10476
|
const filterActive = (0, react.useMemo)(() => activeFilterCount(filters) > 0, [filters]);
|
|
9270
|
-
const
|
|
10477
|
+
const selectionScope = JSON.stringify({
|
|
10478
|
+
...filters,
|
|
10479
|
+
sort: void 0
|
|
10480
|
+
});
|
|
10481
|
+
const downloadsRef = (0, react.useRef)(downloads);
|
|
10482
|
+
downloadsRef.current = downloads;
|
|
10483
|
+
const statesRef = (0, react.useRef)({});
|
|
10484
|
+
statesRef.current = detail?.states ?? {};
|
|
9271
10485
|
(0, react.useEffect)(() => {
|
|
9272
10486
|
setSelected(/* @__PURE__ */ new Set());
|
|
9273
|
-
}, [taskId,
|
|
10487
|
+
}, [taskId, selectionScope]);
|
|
10488
|
+
(0, react.useEffect)(() => {
|
|
10489
|
+
papersByIdRef.current.clear();
|
|
10490
|
+
}, [taskId]);
|
|
10491
|
+
(0, react.useEffect)(() => {
|
|
10492
|
+
for (const paper of view.page?.papers ?? []) papersByIdRef.current.set(paper.paperId, paper);
|
|
10493
|
+
}, [view.page]);
|
|
10494
|
+
(0, react.useEffect)(() => {
|
|
10495
|
+
setDownloads({});
|
|
10496
|
+
setBatchItems([]);
|
|
10497
|
+
setBatchOpen(false);
|
|
10498
|
+
}, [taskId]);
|
|
9274
10499
|
(0, react.useEffect)(() => {
|
|
9275
10500
|
setEditingConditions(false);
|
|
9276
10501
|
setRisFallback(null);
|
|
@@ -9293,64 +10518,100 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9293
10518
|
}, [taskId]);
|
|
9294
10519
|
(0, react.useEffect)(() => {
|
|
9295
10520
|
if (toast === "") return void 0;
|
|
9296
|
-
const timer = setTimeout(() =>
|
|
10521
|
+
const timer = setTimeout(() => notify(""), toastMs);
|
|
9297
10522
|
return () => clearTimeout(timer);
|
|
9298
|
-
}, [toast]);
|
|
10523
|
+
}, [toast, toastMs]);
|
|
9299
10524
|
(0, react.useEffect)(() => {
|
|
9300
10525
|
bodyRef.current?.scrollTo({ top: 0 });
|
|
9301
10526
|
}, [view.pageIndex, tab]);
|
|
9302
10527
|
const insert = (paper) => {
|
|
9303
10528
|
const outcome = insertQuote(inputActions, paperText(t, paper), paperSummary(t, paper));
|
|
9304
|
-
if (outcome === "inserted")
|
|
9305
|
-
else
|
|
10529
|
+
if (outcome === "inserted") notify(t("result.inserted"));
|
|
10530
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
9306
10531
|
};
|
|
10532
|
+
/** 跨页选中也要整批插入:优先用翻页时缓存的论文,缺的再按 id 补详情。 */
|
|
9307
10533
|
const insertMany = () => {
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
const
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
10534
|
+
if (taskId === null || ids.length === 0) return;
|
|
10535
|
+
const known = papersByIdRef.current;
|
|
10536
|
+
const finish = () => {
|
|
10537
|
+
const papers = ids.map((paperId) => known.get(paperId)).filter((paper) => paper !== void 0);
|
|
10538
|
+
if (papers.length === 0) return;
|
|
10539
|
+
const outcome = insertQuote(inputActions, papersText(t, papers), papersSummary(t, papers));
|
|
10540
|
+
if (outcome === "inserted") notify(tpl(t("bulk.inserted"), { n: papers.length }));
|
|
10541
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
10542
|
+
};
|
|
10543
|
+
const missing = ids.filter((paperId) => !known.has(paperId));
|
|
10544
|
+
if (missing.length === 0) {
|
|
10545
|
+
finish();
|
|
10546
|
+
return;
|
|
10547
|
+
}
|
|
10548
|
+
Promise.all(missing.map((paperId) => fetchPaper(taskId, paperId).then((loaded) => {
|
|
10549
|
+
known.set(paperId, loaded.paper);
|
|
10550
|
+
}).catch(() => void 0))).then(finish);
|
|
9315
10551
|
};
|
|
9316
10552
|
/** 把任务 id 与当前条件带入会话草稿,新会话也能续接同一任务。 */
|
|
9317
10553
|
const insertTask = () => {
|
|
9318
10554
|
if (detail === null) return;
|
|
9319
10555
|
const outcome = insertQuote(inputActions, taskText(t, detail), taskSummary(t, detail));
|
|
9320
|
-
if (outcome === "inserted")
|
|
9321
|
-
else
|
|
10556
|
+
if (outcome === "inserted") notify(t("task.inserted"));
|
|
10557
|
+
else notify(t(outcome === "no-editor" ? "error.noEditor" : "error.draftChanged"));
|
|
9322
10558
|
};
|
|
9323
10559
|
const downloadPaper = (paper) => {
|
|
9324
10560
|
if (taskId === null) return;
|
|
10561
|
+
const paperId = paper.paperId;
|
|
10562
|
+
setDownloads((previous) => ({
|
|
10563
|
+
...previous,
|
|
10564
|
+
[paperId]: { state: "downloading" }
|
|
10565
|
+
}));
|
|
9325
10566
|
(async () => {
|
|
9326
10567
|
try {
|
|
9327
|
-
const result = await downloadPaperPdf(taskId,
|
|
9328
|
-
if (result.status === "downloaded")
|
|
9329
|
-
|
|
9330
|
-
|
|
10568
|
+
const result = await downloadPaperPdf(taskId, paperId);
|
|
10569
|
+
if (result.status === "downloaded") {
|
|
10570
|
+
setDownloads((previous) => ({
|
|
10571
|
+
...previous,
|
|
10572
|
+
[paperId]: {
|
|
10573
|
+
state: "downloaded",
|
|
10574
|
+
...result.path === void 0 ? {} : { path: result.path }
|
|
10575
|
+
}
|
|
10576
|
+
}));
|
|
10577
|
+
notify(tpl(t("pdf.downloaded"), { name: result.filename ?? "" }), "info", 6e3);
|
|
10578
|
+
} else {
|
|
10579
|
+
setDownloads((previous) => ({
|
|
10580
|
+
...previous,
|
|
10581
|
+
[paperId]: {
|
|
10582
|
+
state: "failed",
|
|
10583
|
+
error: result.error ?? result.status,
|
|
10584
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
10585
|
+
}
|
|
10586
|
+
}));
|
|
10587
|
+
notify(result.status === "html-only" ? tpl(t("pdf.htmlFallback"), { message: result.error ?? "" }) : tpl(t("pdf.error"), { message: result.error ?? result.status }), "error", 8e3);
|
|
10588
|
+
}
|
|
9331
10589
|
} catch (error) {
|
|
9332
|
-
|
|
10590
|
+
setDownloads((previous) => ({
|
|
10591
|
+
...previous,
|
|
10592
|
+
[paperId]: {
|
|
10593
|
+
state: "failed",
|
|
10594
|
+
error: messageOf(error)
|
|
10595
|
+
}
|
|
10596
|
+
}));
|
|
10597
|
+
notify(tpl(t("pdf.error"), { message: messageOf(error) }), "error", 8e3);
|
|
9333
10598
|
} finally {
|
|
9334
10599
|
view.reload();
|
|
9335
10600
|
}
|
|
9336
10601
|
})();
|
|
9337
10602
|
};
|
|
10603
|
+
/** 单篇 / 批量 / 模型指令共用一个弹窗:先选 Zotero 目录,再逐篇同步。 */
|
|
10604
|
+
const openZotero = (nextItems, meta) => {
|
|
10605
|
+
if (taskId === null || nextItems.length === 0) return;
|
|
10606
|
+
setZoteroItems(nextItems);
|
|
10607
|
+
setZoteroMeta(meta);
|
|
10608
|
+
setZoteroOpen(true);
|
|
10609
|
+
};
|
|
9338
10610
|
const zoteroPaper = (paper) => {
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
ids: [paper.paperId],
|
|
9344
|
-
message: result.messages[0] ?? t("pdf.zoteroFailed")
|
|
9345
|
-
});
|
|
9346
|
-
}).catch((error) => {
|
|
9347
|
-
const message = messageOf(error);
|
|
9348
|
-
setToast(tpl(t("pdf.zoteroFailed"), { message }));
|
|
9349
|
-
setRisFallback({
|
|
9350
|
-
ids: [paper.paperId],
|
|
9351
|
-
message
|
|
9352
|
-
});
|
|
9353
|
-
});
|
|
10611
|
+
openZotero([{
|
|
10612
|
+
paperId: paper.paperId,
|
|
10613
|
+
title: paper.title
|
|
10614
|
+
}]);
|
|
9354
10615
|
};
|
|
9355
10616
|
const toggleSelect = (paperId) => {
|
|
9356
10617
|
setSelected((previous) => {
|
|
@@ -9360,7 +10621,17 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9360
10621
|
return next;
|
|
9361
10622
|
});
|
|
9362
10623
|
};
|
|
9363
|
-
|
|
10624
|
+
/** 本页全选用并集:翻页可累加,不会把其它页已选的文献冲掉。 */
|
|
10625
|
+
const selectPage = (pageIds) => {
|
|
10626
|
+
if (pageIds.length === 0) return;
|
|
10627
|
+
setSelected((previous) => {
|
|
10628
|
+
const next = new Set(previous);
|
|
10629
|
+
for (const paperId of pageIds) next.add(paperId);
|
|
10630
|
+
return next;
|
|
10631
|
+
});
|
|
10632
|
+
};
|
|
10633
|
+
/** 取消全选:清空所有已选(含其它页)。 */
|
|
10634
|
+
const clearSelection = () => setSelected(/* @__PURE__ */ new Set());
|
|
9364
10635
|
const annotateMany = (patch) => {
|
|
9365
10636
|
if (ids.length === 0) return;
|
|
9366
10637
|
view.annotate(ids.map((paperId) => ({
|
|
@@ -9380,44 +10651,230 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9380
10651
|
})));
|
|
9381
10652
|
})();
|
|
9382
10653
|
};
|
|
9383
|
-
/**
|
|
9384
|
-
|
|
9385
|
-
|
|
10654
|
+
/**
|
|
10655
|
+
* 批量下载:打开进度弹窗后按最多 5 篇并发下载。
|
|
10656
|
+
*
|
|
10657
|
+
* 已下载(含文件已删除以外的正常已下载)的文献直接略过,不再重复请求;
|
|
10658
|
+
* 每篇的结果实时写入 downloads,弹窗只做集中展示。批量未结束时再次触发
|
|
10659
|
+
* 只重新打开弹窗,避免两轮循环重复下载同一批文献。
|
|
10660
|
+
*/
|
|
10661
|
+
const startBatchDownload = (entries, meta = { origin: "panel" }) => {
|
|
10662
|
+
if (taskId === null || entries.length === 0) return;
|
|
10663
|
+
if (batchRunningRef.current) {
|
|
10664
|
+
setBatchOpen(true);
|
|
10665
|
+
return;
|
|
10666
|
+
}
|
|
10667
|
+
const controller = new AbortController();
|
|
10668
|
+
batchAbortRef.current = controller;
|
|
10669
|
+
setBatchCancelled(false);
|
|
10670
|
+
setBatchMeta(meta);
|
|
10671
|
+
setBatchItems(entries.map((entry) => ({ ...entry })));
|
|
10672
|
+
setBatchOpen(true);
|
|
10673
|
+
batchRunningRef.current = true;
|
|
10674
|
+
setBatchRunning(true);
|
|
9386
10675
|
(async () => {
|
|
9387
10676
|
let ok = 0;
|
|
9388
|
-
let
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
10677
|
+
let manualCount = 0;
|
|
10678
|
+
let failedCount = 0;
|
|
10679
|
+
let changed = false;
|
|
10680
|
+
/** 单篇:跳过已下载的,下载结果实时写回 downloads。 */
|
|
10681
|
+
const runOne = async (paperId) => {
|
|
10682
|
+
const runtime = downloadsRef.current[paperId];
|
|
10683
|
+
if (runtime?.state === "downloading") return;
|
|
10684
|
+
if (runtime?.state === "downloaded" || statesRef.current[paperId]?.pdfStatus === "downloaded") return;
|
|
10685
|
+
changed = true;
|
|
10686
|
+
setDownloads((previous) => ({
|
|
10687
|
+
...previous,
|
|
10688
|
+
[paperId]: { state: "downloading" }
|
|
10689
|
+
}));
|
|
10690
|
+
try {
|
|
10691
|
+
const result = await downloadPaperPdf(taskId, paperId, controller.signal);
|
|
10692
|
+
if (result.status === "downloaded") {
|
|
10693
|
+
ok += 1;
|
|
10694
|
+
setDownloads((previous) => ({
|
|
10695
|
+
...previous,
|
|
10696
|
+
[paperId]: {
|
|
10697
|
+
state: "downloaded",
|
|
10698
|
+
...result.path === void 0 ? {} : { path: result.path }
|
|
10699
|
+
}
|
|
10700
|
+
}));
|
|
10701
|
+
} else {
|
|
10702
|
+
if (isManualPdfFailure(result.reason)) manualCount += 1;
|
|
10703
|
+
else failedCount += 1;
|
|
10704
|
+
setDownloads((previous) => ({
|
|
10705
|
+
...previous,
|
|
10706
|
+
[paperId]: {
|
|
10707
|
+
state: "failed",
|
|
10708
|
+
error: result.error ?? result.status,
|
|
10709
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
10710
|
+
}
|
|
10711
|
+
}));
|
|
10712
|
+
}
|
|
10713
|
+
} catch (error) {
|
|
10714
|
+
if (controller.signal.aborted) {
|
|
10715
|
+
setDownloads((previous) => {
|
|
10716
|
+
const next = { ...previous };
|
|
10717
|
+
delete next[paperId];
|
|
10718
|
+
return next;
|
|
10719
|
+
});
|
|
10720
|
+
return;
|
|
10721
|
+
}
|
|
10722
|
+
failedCount += 1;
|
|
10723
|
+
setDownloads((previous) => ({
|
|
10724
|
+
...previous,
|
|
10725
|
+
[paperId]: {
|
|
10726
|
+
state: "failed",
|
|
10727
|
+
error: messageOf(error)
|
|
10728
|
+
}
|
|
10729
|
+
}));
|
|
10730
|
+
}
|
|
10731
|
+
};
|
|
10732
|
+
let cursor = 0;
|
|
10733
|
+
const worker = async () => {
|
|
10734
|
+
while (cursor < entries.length) {
|
|
10735
|
+
if (controller.signal.aborted) return;
|
|
10736
|
+
const entry = entries[cursor];
|
|
10737
|
+
cursor += 1;
|
|
10738
|
+
if (entry !== void 0) await runOne(entry.paperId);
|
|
10739
|
+
}
|
|
10740
|
+
};
|
|
10741
|
+
try {
|
|
10742
|
+
await Promise.all(Array.from({ length: Math.min(BATCH_CONCURRENCY, entries.length) }, () => worker()));
|
|
10743
|
+
} finally {
|
|
10744
|
+
batchRunningRef.current = false;
|
|
10745
|
+
batchAbortRef.current = null;
|
|
10746
|
+
setBatchRunning(false);
|
|
10747
|
+
if (changed) view.reload();
|
|
9395
10748
|
}
|
|
9396
|
-
|
|
10749
|
+
if (changed && !controller.signal.aborted) notify(tpl(t("bulk.downloadDone"), {
|
|
9397
10750
|
ok,
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
10751
|
+
manual: manualCount,
|
|
10752
|
+
failed: failedCount
|
|
10753
|
+
}), failedCount > 0 ? "error" : "info", 8e3);
|
|
9401
10754
|
})();
|
|
9402
10755
|
};
|
|
10756
|
+
/** 结果页选中项:标题优先取当前页,跨页选中的文献再异步补详情。 */
|
|
10757
|
+
const downloadMany = () => {
|
|
10758
|
+
if (taskId === null || ids.length === 0) return;
|
|
10759
|
+
const known = /* @__PURE__ */ new Map();
|
|
10760
|
+
for (const paper of view.page?.papers ?? []) known.set(paper.paperId, paper.title);
|
|
10761
|
+
startBatchDownload(ids.map((paperId) => ({
|
|
10762
|
+
paperId,
|
|
10763
|
+
title: known.get(paperId) ?? ""
|
|
10764
|
+
})));
|
|
10765
|
+
const missing = ids.filter((paperId) => !known.has(paperId));
|
|
10766
|
+
if (missing.length === 0) return;
|
|
10767
|
+
Promise.all(missing.map((paperId) => fetchPaper(taskId, paperId).then((loaded) => {
|
|
10768
|
+
setBatchItems((previous) => previous.map((item) => item.paperId === paperId ? {
|
|
10769
|
+
...item,
|
|
10770
|
+
title: loaded.paper.title
|
|
10771
|
+
} : item));
|
|
10772
|
+
}).catch(() => void 0)));
|
|
10773
|
+
};
|
|
10774
|
+
/** 只有显式的「取消下载」才中止;关闭弹窗仍只隐藏,后台继续跑完。 */
|
|
10775
|
+
const cancelBatch = () => {
|
|
10776
|
+
batchAbortRef.current?.abort();
|
|
10777
|
+
setBatchCancelled(true);
|
|
10778
|
+
};
|
|
10779
|
+
(0, react.useEffect)(() => {
|
|
10780
|
+
if (taskId === null || view.detail === null || view.page === null) return void 0;
|
|
10781
|
+
const body = {
|
|
10782
|
+
taskId,
|
|
10783
|
+
pageIndex: view.pageIndex,
|
|
10784
|
+
pageSize: view.pageSize,
|
|
10785
|
+
total: view.page.total,
|
|
10786
|
+
...filters.sort === void 0 ? {} : { sort: filters.sort },
|
|
10787
|
+
filters: snapshotFilters(filters),
|
|
10788
|
+
pagePaperIds: view.page.papers.map((paper) => paper.paperId),
|
|
10789
|
+
selectedIds: [...selected],
|
|
10790
|
+
rev: view.page.rev
|
|
10791
|
+
};
|
|
10792
|
+
const timer = setTimeout(() => {
|
|
10793
|
+
if (document.hidden) return;
|
|
10794
|
+
postViewSnapshot(body).catch(() => void 0);
|
|
10795
|
+
}, 400);
|
|
10796
|
+
return () => clearTimeout(timer);
|
|
10797
|
+
}, [
|
|
10798
|
+
taskId,
|
|
10799
|
+
view.detail,
|
|
10800
|
+
view.page,
|
|
10801
|
+
view.pageIndex,
|
|
10802
|
+
view.pageSize,
|
|
10803
|
+
filters,
|
|
10804
|
+
selected
|
|
10805
|
+
]);
|
|
10806
|
+
(0, react.useEffect)(() => {
|
|
10807
|
+
if (taskId === null || batchRunningRef.current || batchRunning) return;
|
|
10808
|
+
const command = pendingCommands[0];
|
|
10809
|
+
if (command === void 0) return;
|
|
10810
|
+
if (command.paperIds.length === 0) {
|
|
10811
|
+
markConsumed(command.commandId);
|
|
10812
|
+
notify(t("batch.empty"));
|
|
10813
|
+
return;
|
|
10814
|
+
}
|
|
10815
|
+
const byId = new Map(command.items.map((item) => [item.paperId, item]));
|
|
10816
|
+
const items = command.paperIds.map((paperId) => {
|
|
10817
|
+
const item = byId.get(paperId);
|
|
10818
|
+
return {
|
|
10819
|
+
paperId,
|
|
10820
|
+
title: item?.title ?? papersByIdRef.current.get(paperId)?.title ?? "",
|
|
10821
|
+
...item?.source === void 0 ? {} : { source: item.source },
|
|
10822
|
+
...item?.meta === void 0 ? {} : { meta: item.meta }
|
|
10823
|
+
};
|
|
10824
|
+
});
|
|
10825
|
+
markConsumed(command.commandId);
|
|
10826
|
+
startBatchDownload(items, {
|
|
10827
|
+
origin: "model",
|
|
10828
|
+
scopeLabel: command.scopeLabel,
|
|
10829
|
+
reason: command.reason
|
|
10830
|
+
});
|
|
10831
|
+
}, [
|
|
10832
|
+
taskId,
|
|
10833
|
+
pendingCommands,
|
|
10834
|
+
batchRunning
|
|
10835
|
+
]);
|
|
10836
|
+
(0, react.useEffect)(() => {
|
|
10837
|
+
if (taskId === null || zoteroOpen) return;
|
|
10838
|
+
const command = pendingZoteroCommands[0];
|
|
10839
|
+
if (command === void 0) return;
|
|
10840
|
+
if (command.paperIds.length === 0) {
|
|
10841
|
+
markZoteroConsumed(command.commandId);
|
|
10842
|
+
notify(t("zotero.dialog.modelEmpty"));
|
|
10843
|
+
return;
|
|
10844
|
+
}
|
|
10845
|
+
const byId = new Map(command.items.map((item) => [item.paperId, item]));
|
|
10846
|
+
const items = command.paperIds.map((paperId) => {
|
|
10847
|
+
const item = byId.get(paperId);
|
|
10848
|
+
return {
|
|
10849
|
+
paperId,
|
|
10850
|
+
title: item?.title ?? papersByIdRef.current.get(paperId)?.title ?? "",
|
|
10851
|
+
...item?.source === void 0 ? {} : { source: item.source },
|
|
10852
|
+
...item?.meta === void 0 ? {} : { meta: item.meta }
|
|
10853
|
+
};
|
|
10854
|
+
});
|
|
10855
|
+
markZoteroConsumed(command.commandId);
|
|
10856
|
+
openZotero(items, {
|
|
10857
|
+
origin: "model",
|
|
10858
|
+
scopeLabel: command.scopeLabel,
|
|
10859
|
+
reason: command.reason,
|
|
10860
|
+
...command.target === void 0 ? {} : { initialTarget: command.target },
|
|
10861
|
+
autoStart: command.autoStart
|
|
10862
|
+
});
|
|
10863
|
+
}, [
|
|
10864
|
+
taskId,
|
|
10865
|
+
pendingZoteroCommands,
|
|
10866
|
+
zoteroOpen
|
|
10867
|
+
]);
|
|
9403
10868
|
const zoteroIds = (paperIds) => {
|
|
9404
10869
|
if (taskId === null || paperIds.length === 0) return;
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
})
|
|
9412
|
-
const message = messageOf(error);
|
|
9413
|
-
setToast(tpl(t("pdf.zoteroFailed"), { message }));
|
|
9414
|
-
setRisFallback({
|
|
9415
|
-
ids: [...paperIds],
|
|
9416
|
-
message
|
|
9417
|
-
});
|
|
9418
|
-
});
|
|
10870
|
+
const known = /* @__PURE__ */ new Map();
|
|
10871
|
+
for (const paper of view.page?.papers ?? []) known.set(paper.paperId, paper.title);
|
|
10872
|
+
for (const paper of papersByIdRef.current.values()) if (!known.has(paper.paperId)) known.set(paper.paperId, paper.title);
|
|
10873
|
+
openZotero(paperIds.map((paperId) => ({
|
|
10874
|
+
paperId,
|
|
10875
|
+
title: known.get(paperId) ?? ""
|
|
10876
|
+
})));
|
|
9419
10877
|
};
|
|
9420
|
-
const downloadMany = () => downloadIds(ids);
|
|
9421
10878
|
const zoteroMany = () => zoteroIds(ids);
|
|
9422
10879
|
if (taskId === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9423
10880
|
style: panelStyle,
|
|
@@ -9447,11 +10904,12 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9447
10904
|
const failed = detail.result.sources.filter((source) => !source.ok && !source.skipped);
|
|
9448
10905
|
const skipped = detail.result.sources.filter((source) => source.skipped);
|
|
9449
10906
|
const page = view.page;
|
|
10907
|
+
const pageIds = page?.papers.map((paper) => paper.paperId) ?? [];
|
|
9450
10908
|
/** 当前生效的排序:面板选择优先,未选择时跟随任务条件。 */
|
|
9451
10909
|
const sortValue = filters.sort ?? detail.conditions.sort;
|
|
9452
10910
|
const coverage = overview?.fullText;
|
|
9453
10911
|
const overviewAvailable = coverage === void 0 ? 0 : coverage.available + coverage.downloaded + coverage.htmlOnly;
|
|
9454
|
-
const overviewMissing = coverage === void 0 ? 0 : coverage.unknown + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
10912
|
+
const overviewMissing = coverage === void 0 ? 0 : coverage.unknown + coverage.missing + coverage.paywalled + coverage.blocked + coverage.failed;
|
|
9455
10913
|
const overviewRate = overview === null || overview.loaded === 0 ? 0 : Math.round(overviewAvailable / overview.loaded * 100);
|
|
9456
10914
|
const conditionMeta = [tpl(t("task.pageSize"), { n: detail.conditions.pageSize })];
|
|
9457
10915
|
if (detail.conditions.yearFrom !== void 0 || detail.conditions.yearTo !== void 0) conditionMeta.push(tpl(t("task.conditionYear"), { value: `${detail.conditions.yearFrom ?? ""}–${detail.conditions.yearTo ?? ""}` }));
|
|
@@ -9723,34 +11181,40 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9723
11181
|
onCancel: () => setEditingConditions(false),
|
|
9724
11182
|
onSaved: () => {
|
|
9725
11183
|
setEditingConditions(false);
|
|
9726
|
-
|
|
11184
|
+
notify(t("condition.saved"));
|
|
9727
11185
|
view.reload();
|
|
9728
11186
|
}
|
|
9729
11187
|
}),
|
|
9730
|
-
toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
9731
|
-
style:
|
|
9732
|
-
children: toast
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
11188
|
+
(toast !== "" || risFallback !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11189
|
+
style: toastStackStyle,
|
|
11190
|
+
children: [toast !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11191
|
+
role: "status",
|
|
11192
|
+
style: {
|
|
11193
|
+
...toastStyle,
|
|
11194
|
+
...toastTone === "error" ? toastErrorStyle : null
|
|
11195
|
+
},
|
|
11196
|
+
children: toast
|
|
11197
|
+
}), risFallback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11198
|
+
style: RIS_FALLBACK_STYLE,
|
|
11199
|
+
children: [
|
|
11200
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11201
|
+
style: { minWidth: 0 },
|
|
11202
|
+
children: tpl(t("pdf.zoteroFallback"), { message: risFallback.message })
|
|
11203
|
+
}),
|
|
11204
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
11205
|
+
href: exportUrl(taskId, "ris", {}, risFallback.ids),
|
|
11206
|
+
download: true,
|
|
11207
|
+
style: RIS_FALLBACK_LINK_STYLE,
|
|
11208
|
+
children: t("pdf.zoteroFallbackLink")
|
|
11209
|
+
}),
|
|
11210
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11211
|
+
type: "button",
|
|
11212
|
+
style: RIS_FALLBACK_BUTTON_STYLE,
|
|
11213
|
+
onClick: () => setRisFallback(null),
|
|
11214
|
+
children: t("pdf.zoteroFallbackDismiss")
|
|
11215
|
+
})
|
|
11216
|
+
]
|
|
11217
|
+
})]
|
|
9754
11218
|
}),
|
|
9755
11219
|
view.error !== null && detail.result.totalLoaded > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9756
11220
|
style: {
|
|
@@ -9846,23 +11310,48 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9846
11310
|
}),
|
|
9847
11311
|
page !== null && page.total > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9848
11312
|
style: PAGE_TOOLBAR_ROW_STYLE,
|
|
9849
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
9850
|
-
style:
|
|
9851
|
-
children: [
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
11313
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11314
|
+
style: PAGE_TOOLBAR_LEFT_STYLE,
|
|
11315
|
+
children: [
|
|
11316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
11317
|
+
style: PAGE_SORT_LABEL_STYLE,
|
|
11318
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("filter.sort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
11319
|
+
style: PAGE_SORT_SELECT_STYLE,
|
|
11320
|
+
value: sortValue,
|
|
11321
|
+
onChange: (event) => {
|
|
11322
|
+
const next = event.target.value;
|
|
11323
|
+
setFilters((previous) => ({
|
|
11324
|
+
...previous,
|
|
11325
|
+
sort: next === detail.conditions.sort ? void 0 : next
|
|
11326
|
+
}));
|
|
11327
|
+
},
|
|
11328
|
+
children: SORT_MODES.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
11329
|
+
value: mode,
|
|
11330
|
+
children: t(`sort.${mode}`)
|
|
11331
|
+
}, mode))
|
|
11332
|
+
})]
|
|
11333
|
+
}),
|
|
11334
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11335
|
+
type: "button",
|
|
11336
|
+
style: PAGE_TOOLBAR_ACTION_STYLE,
|
|
11337
|
+
title: t("bulk.selectPageHint"),
|
|
11338
|
+
onClick: () => selectPage(pageIds),
|
|
11339
|
+
children: t("bulk.selectPage")
|
|
11340
|
+
}),
|
|
11341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11342
|
+
type: "button",
|
|
11343
|
+
style: selected.size === 0 ? PAGE_TOOLBAR_ACTION_DISABLED_STYLE : PAGE_TOOLBAR_ACTION_STYLE,
|
|
11344
|
+
disabled: selected.size === 0,
|
|
11345
|
+
title: t("bulk.unselectAllHint"),
|
|
11346
|
+
onClick: clearSelection,
|
|
11347
|
+
children: t("bulk.unselectAll")
|
|
11348
|
+
}),
|
|
11349
|
+
selected.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11350
|
+
style: PAGE_TOOLBAR_COUNT_STYLE,
|
|
11351
|
+
title: t("bulk.keepHint"),
|
|
11352
|
+
children: tpl(t("bulk.count"), { n: selected.size })
|
|
11353
|
+
})
|
|
11354
|
+
]
|
|
9866
11355
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pager, {
|
|
9867
11356
|
t,
|
|
9868
11357
|
index: view.pageIndex,
|
|
@@ -9876,16 +11365,21 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9876
11365
|
taskId,
|
|
9877
11366
|
count: selected.size,
|
|
9878
11367
|
ids,
|
|
9879
|
-
pageIds
|
|
11368
|
+
pageIds,
|
|
9880
11369
|
groups: detail.groups,
|
|
9881
11370
|
busy: view.busy,
|
|
9882
11371
|
filters,
|
|
9883
|
-
onClear:
|
|
9884
|
-
onSelectPage:
|
|
11372
|
+
onClear: clearSelection,
|
|
11373
|
+
onSelectPage: selectPage,
|
|
9885
11374
|
onAnnotate: annotateMany,
|
|
9886
11375
|
onInsert: insertMany,
|
|
9887
11376
|
onDownload: downloadMany,
|
|
9888
|
-
onZotero: zoteroMany
|
|
11377
|
+
onZotero: zoteroMany,
|
|
11378
|
+
batchRunning,
|
|
11379
|
+
...detail.downloadDir === void 0 || onRevealLocal === void 0 ? {} : {
|
|
11380
|
+
downloadDir: detail.downloadDir,
|
|
11381
|
+
onRevealDir: onRevealLocal
|
|
11382
|
+
}
|
|
9889
11383
|
}),
|
|
9890
11384
|
page !== null && page.papers.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
9891
11385
|
style: listStyle$1,
|
|
@@ -9896,6 +11390,10 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9896
11390
|
state: detail.states[paper.paperId],
|
|
9897
11391
|
groups: detail.groups,
|
|
9898
11392
|
busy: view.busy,
|
|
11393
|
+
...detail.downloadDir === void 0 ? {} : { downloadDir: detail.downloadDir },
|
|
11394
|
+
...downloads[paper.paperId] === void 0 ? {} : { download: downloads[paper.paperId] },
|
|
11395
|
+
...onOpenLocal === void 0 ? {} : { onOpenLocal },
|
|
11396
|
+
...onRevealLocal === void 0 ? {} : { onRevealLocal },
|
|
9899
11397
|
selected: selected.has(paper.paperId),
|
|
9900
11398
|
onToggleSelect: toggleSelect,
|
|
9901
11399
|
onInsert: insert,
|
|
@@ -9946,8 +11444,13 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9946
11444
|
states: detail.states,
|
|
9947
11445
|
stats: overview,
|
|
9948
11446
|
onDownload: downloadPaper,
|
|
11447
|
+
...onOpenLocal === void 0 ? {} : { onOpenLocal },
|
|
11448
|
+
...onRevealLocal === void 0 ? {} : { onRevealLocal },
|
|
9949
11449
|
onZotero: zoteroPaper,
|
|
9950
|
-
onBatchDownload: (papers) =>
|
|
11450
|
+
onBatchDownload: (papers) => startBatchDownload(papers.map((paper) => ({
|
|
11451
|
+
paperId: paper.paperId,
|
|
11452
|
+
title: paper.title
|
|
11453
|
+
}))),
|
|
9951
11454
|
onBatchZotero: (papers) => zoteroIds(papers.map((paper) => paper.paperId)),
|
|
9952
11455
|
onShowInResults: (viewKey) => {
|
|
9953
11456
|
setFilters(viewKey === "missing" ? { missingFullText: true } : { pdfStatus: viewKey });
|
|
@@ -9958,6 +11461,7 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9958
11461
|
taskId,
|
|
9959
11462
|
detail,
|
|
9960
11463
|
onSaved: view.reload,
|
|
11464
|
+
onNotify: notify,
|
|
9961
11465
|
onPickGroup: (groupId) => {
|
|
9962
11466
|
setFilters((previous) => ({
|
|
9963
11467
|
...previous,
|
|
@@ -9969,7 +11473,8 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9969
11473
|
t,
|
|
9970
11474
|
taskId,
|
|
9971
11475
|
detail,
|
|
9972
|
-
onSaved: view.reload
|
|
11476
|
+
onSaved: view.reload,
|
|
11477
|
+
onNotify: notify
|
|
9973
11478
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompareTab, {
|
|
9974
11479
|
t,
|
|
9975
11480
|
taskId,
|
|
@@ -9986,6 +11491,42 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9986
11491
|
}
|
|
9987
11492
|
})
|
|
9988
11493
|
}),
|
|
11494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadDialog, {
|
|
11495
|
+
t,
|
|
11496
|
+
open: batchOpen,
|
|
11497
|
+
items: batchItems,
|
|
11498
|
+
states: detail.states,
|
|
11499
|
+
downloads,
|
|
11500
|
+
origin: batchMeta.origin,
|
|
11501
|
+
scopeLabel: batchMeta.scopeLabel,
|
|
11502
|
+
reason: batchMeta.reason,
|
|
11503
|
+
cancelled: batchCancelled,
|
|
11504
|
+
onCancel: batchMeta.origin === "model" ? cancelBatch : void 0,
|
|
11505
|
+
onClose: () => {
|
|
11506
|
+
setBatchOpen(false);
|
|
11507
|
+
view.reload();
|
|
11508
|
+
}
|
|
11509
|
+
}),
|
|
11510
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoteroSyncDialog, {
|
|
11511
|
+
t,
|
|
11512
|
+
open: zoteroOpen,
|
|
11513
|
+
taskId,
|
|
11514
|
+
items: zoteroItems,
|
|
11515
|
+
states: detail.states,
|
|
11516
|
+
meta: zoteroMeta,
|
|
11517
|
+
onClose: () => {
|
|
11518
|
+
setZoteroOpen(false);
|
|
11519
|
+
view.reload();
|
|
11520
|
+
},
|
|
11521
|
+
onDone: (failedIds) => {
|
|
11522
|
+
view.reload();
|
|
11523
|
+
if (failedIds.length === 0) return;
|
|
11524
|
+
setRisFallback({
|
|
11525
|
+
ids: failedIds,
|
|
11526
|
+
message: t("zotero.dialog.failFallback")
|
|
11527
|
+
});
|
|
11528
|
+
}
|
|
11529
|
+
}),
|
|
9989
11530
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundsDialog, {
|
|
9990
11531
|
t,
|
|
9991
11532
|
open: showRounds,
|
|
@@ -9995,7 +11536,7 @@ function TaskPane({ t, useTabInfo, inputActions, onOpenPaper, onOpenTask, onClos
|
|
|
9995
11536
|
busy: active,
|
|
9996
11537
|
onRerun: () => {
|
|
9997
11538
|
view.reload();
|
|
9998
|
-
|
|
11539
|
+
notify(t("rounds.rerunStarted"));
|
|
9999
11540
|
}
|
|
10000
11541
|
}),
|
|
10001
11542
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskListDialog, {
|
|
@@ -10154,6 +11695,8 @@ const EXPORT_FORMATS = [
|
|
|
10154
11695
|
"bibtex",
|
|
10155
11696
|
"md"
|
|
10156
11697
|
];
|
|
11698
|
+
/** 批量下载的并发上限:同时下载 5 篇。 */
|
|
11699
|
+
const BATCH_CONCURRENCY = 5;
|
|
10157
11700
|
/** 筛读计数(点击筛选):与「筛选」按钮同排,接在筛选条最前面。 */
|
|
10158
11701
|
function ScreeningPills({ t, detail, filters, onChange }) {
|
|
10159
11702
|
const counts = detail.stats.screening;
|
|
@@ -10186,7 +11729,7 @@ function ScreeningPills({ t, detail, filters, onChange }) {
|
|
|
10186
11729
|
});
|
|
10187
11730
|
}
|
|
10188
11731
|
/** 选中多篇后的批量操作条:筛读、分组、下载、Zotero、按选中导出。 */
|
|
10189
|
-
function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, filters, onClear, onSelectPage, onAnnotate, onInsert, onDownload, onZotero }) {
|
|
11732
|
+
function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, batchRunning, downloadDir, onRevealDir, filters, onClear, onSelectPage, onAnnotate, onInsert, onDownload, onZotero }) {
|
|
10190
11733
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10191
11734
|
style: BULK_STYLE,
|
|
10192
11735
|
children: [
|
|
@@ -10261,11 +11804,15 @@ function BulkBar({ t, taskId, count, ids, pageIds, groups, busy, filters, onClea
|
|
|
10261
11804
|
label: t("bulk.insert"),
|
|
10262
11805
|
onClick: onInsert
|
|
10263
11806
|
}),
|
|
10264
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
10265
|
-
|
|
11807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkDownloadButton, {
|
|
11808
|
+
t,
|
|
10266
11809
|
label: t("bulk.download"),
|
|
10267
11810
|
onClick: onDownload,
|
|
10268
|
-
disabled: busy
|
|
11811
|
+
disabled: busy || batchRunning,
|
|
11812
|
+
...downloadDir === void 0 || onRevealDir === void 0 ? {} : {
|
|
11813
|
+
downloadDir,
|
|
11814
|
+
onReveal: onRevealDir
|
|
11815
|
+
}
|
|
10269
11816
|
}),
|
|
10270
11817
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkActionButton, {
|
|
10271
11818
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoteroIcon, {}),
|
|
@@ -10317,6 +11864,73 @@ function BulkActionButton({ icon, label, onClick, disabled = false }) {
|
|
|
10317
11864
|
children: [icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
10318
11865
|
});
|
|
10319
11866
|
}
|
|
11867
|
+
/**
|
|
11868
|
+
* 批量下载按钮 + 下三角:点主体打开下载进度弹窗;点箭头展开菜单,
|
|
11869
|
+
* 在系统文件管理器中打开下载目录(与条目上「查看已下载 ▾」的菜单一致)。
|
|
11870
|
+
*/
|
|
11871
|
+
function BulkDownloadButton({ t, label, onClick, disabled, downloadDir, onReveal }) {
|
|
11872
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
11873
|
+
const groupRef = (0, react.useRef)(null);
|
|
11874
|
+
(0, react.useEffect)(() => {
|
|
11875
|
+
if (!open) return void 0;
|
|
11876
|
+
const closeOnOutside = (event) => {
|
|
11877
|
+
if (groupRef.current !== null && !groupRef.current.contains(event.target)) setOpen(false);
|
|
11878
|
+
};
|
|
11879
|
+
const closeOnEscape = (event) => {
|
|
11880
|
+
if (event.key === "Escape") setOpen(false);
|
|
11881
|
+
};
|
|
11882
|
+
document.addEventListener("mousedown", closeOnOutside);
|
|
11883
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
11884
|
+
return () => {
|
|
11885
|
+
document.removeEventListener("mousedown", closeOnOutside);
|
|
11886
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
11887
|
+
};
|
|
11888
|
+
}, [open]);
|
|
11889
|
+
const revealable = downloadDir !== void 0 && onReveal !== void 0;
|
|
11890
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
11891
|
+
ref: groupRef,
|
|
11892
|
+
style: BULK_DOWNLOAD_GROUP_STYLE,
|
|
11893
|
+
children: [
|
|
11894
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BulkActionButton, {
|
|
11895
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadIcon, {}),
|
|
11896
|
+
label,
|
|
11897
|
+
onClick,
|
|
11898
|
+
disabled
|
|
11899
|
+
}),
|
|
11900
|
+
revealable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11901
|
+
type: "button",
|
|
11902
|
+
style: {
|
|
11903
|
+
...BULK_ARROW_STYLE,
|
|
11904
|
+
...disabled ? {
|
|
11905
|
+
opacity: .45,
|
|
11906
|
+
cursor: "default"
|
|
11907
|
+
} : null
|
|
11908
|
+
},
|
|
11909
|
+
"aria-haspopup": "menu",
|
|
11910
|
+
"aria-expanded": open,
|
|
11911
|
+
"aria-label": t("bulk.revealDir"),
|
|
11912
|
+
title: t("bulk.revealDir"),
|
|
11913
|
+
disabled,
|
|
11914
|
+
onClick: () => setOpen((value) => !value),
|
|
11915
|
+
children: "▾"
|
|
11916
|
+
}),
|
|
11917
|
+
open && revealable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11918
|
+
role: "menu",
|
|
11919
|
+
style: BULK_MENU_STYLE,
|
|
11920
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11921
|
+
type: "button",
|
|
11922
|
+
role: "menuitem",
|
|
11923
|
+
style: BULK_MENU_ITEM_STYLE,
|
|
11924
|
+
onClick: () => {
|
|
11925
|
+
setOpen(false);
|
|
11926
|
+
if (downloadDir !== void 0) onReveal?.(downloadDir);
|
|
11927
|
+
},
|
|
11928
|
+
children: t("bulk.revealDir")
|
|
11929
|
+
})
|
|
11930
|
+
})
|
|
11931
|
+
]
|
|
11932
|
+
});
|
|
11933
|
+
}
|
|
10320
11934
|
/** 结果页翻页条:首页 / 上一页 / 页码 / 下一页 / 末页。单页时不渲染。 */
|
|
10321
11935
|
function Pager({ t, index, count, total, onChange }) {
|
|
10322
11936
|
if (count <= 1) return null;
|
|
@@ -10370,15 +11984,6 @@ function Pager({ t, index, count, total, onChange }) {
|
|
|
10370
11984
|
function messageOf(error) {
|
|
10371
11985
|
return error instanceof Error ? error.message : String(error);
|
|
10372
11986
|
}
|
|
10373
|
-
function zoteroToast(t, saved, failed, messages) {
|
|
10374
|
-
const detail = messages[0] ?? "";
|
|
10375
|
-
if (failed === 0 && detail === "") return tpl(t("pdf.zoteroDone"), { n: saved });
|
|
10376
|
-
return tpl(t("pdf.zoteroPartial"), {
|
|
10377
|
-
saved,
|
|
10378
|
-
failed,
|
|
10379
|
-
message: detail
|
|
10380
|
-
});
|
|
10381
|
-
}
|
|
10382
11987
|
/** 结果页「检索还在进行」的提示条:蓝色高亮块 + 旋转图标。 */
|
|
10383
11988
|
const ROUND_RESULTS_HINT_STYLE = {
|
|
10384
11989
|
display: "flex",
|
|
@@ -10399,16 +12004,23 @@ const ROUND_RESULTS_ICON_STYLE = {
|
|
|
10399
12004
|
flexShrink: 0
|
|
10400
12005
|
};
|
|
10401
12006
|
const RIS_FALLBACK_STYLE = {
|
|
12007
|
+
...toastStyle,
|
|
12008
|
+
pointerEvents: "auto",
|
|
10402
12009
|
display: "flex",
|
|
10403
12010
|
alignItems: "center",
|
|
10404
12011
|
flexWrap: "wrap",
|
|
10405
12012
|
gap: 8,
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
fontSize:
|
|
10411
|
-
|
|
12013
|
+
borderLeft: "3px solid #b26a00"
|
|
12014
|
+
};
|
|
12015
|
+
const RIS_FALLBACK_LINK_STYLE = {
|
|
12016
|
+
color: "#fff",
|
|
12017
|
+
fontSize: 12,
|
|
12018
|
+
textDecoration: "underline"
|
|
12019
|
+
};
|
|
12020
|
+
const RIS_FALLBACK_BUTTON_STYLE = {
|
|
12021
|
+
...linkButtonStyle,
|
|
12022
|
+
color: "#fff",
|
|
12023
|
+
textDecoration: "underline"
|
|
10412
12024
|
};
|
|
10413
12025
|
const TAB_ROW_STYLE = {
|
|
10414
12026
|
display: "flex",
|
|
@@ -10517,6 +12129,50 @@ const BULK_BUTTON_STYLE = {
|
|
|
10517
12129
|
lineHeight: 1.4,
|
|
10518
12130
|
cursor: "pointer"
|
|
10519
12131
|
};
|
|
12132
|
+
/** 批量下载 + 下三角的组合:箭头紧贴按钮右侧,菜单相对整体定位。 */
|
|
12133
|
+
const BULK_DOWNLOAD_GROUP_STYLE = {
|
|
12134
|
+
position: "relative",
|
|
12135
|
+
display: "inline-flex",
|
|
12136
|
+
alignItems: "center",
|
|
12137
|
+
gap: 2
|
|
12138
|
+
};
|
|
12139
|
+
const BULK_ARROW_STYLE = {
|
|
12140
|
+
padding: "2px 3px",
|
|
12141
|
+
borderRadius: 5,
|
|
12142
|
+
border: "1px solid rgba(80,140,255,0.4)",
|
|
12143
|
+
background: "transparent",
|
|
12144
|
+
color: ACCENT,
|
|
12145
|
+
fontSize: 12,
|
|
12146
|
+
lineHeight: 1.4,
|
|
12147
|
+
cursor: "pointer"
|
|
12148
|
+
};
|
|
12149
|
+
const BULK_MENU_STYLE = {
|
|
12150
|
+
position: "absolute",
|
|
12151
|
+
top: "100%",
|
|
12152
|
+
left: 0,
|
|
12153
|
+
marginTop: 3,
|
|
12154
|
+
zIndex: 40,
|
|
12155
|
+
display: "flex",
|
|
12156
|
+
flexDirection: "column",
|
|
12157
|
+
minWidth: 132,
|
|
12158
|
+
padding: 2,
|
|
12159
|
+
borderRadius: 6,
|
|
12160
|
+
border: "1px solid rgba(127,127,127,0.3)",
|
|
12161
|
+
background: "#fff",
|
|
12162
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.18)"
|
|
12163
|
+
};
|
|
12164
|
+
const BULK_MENU_ITEM_STYLE = {
|
|
12165
|
+
padding: "5px 8px",
|
|
12166
|
+
border: "none",
|
|
12167
|
+
borderRadius: 4,
|
|
12168
|
+
background: "transparent",
|
|
12169
|
+
color: "#1f2328",
|
|
12170
|
+
fontSize: 12,
|
|
12171
|
+
lineHeight: 1.4,
|
|
12172
|
+
textAlign: "left",
|
|
12173
|
+
whiteSpace: "nowrap",
|
|
12174
|
+
cursor: "pointer"
|
|
12175
|
+
};
|
|
10520
12176
|
const COVERAGE_WRAP_STYLE = {
|
|
10521
12177
|
display: "flex",
|
|
10522
12178
|
flexDirection: "column",
|
|
@@ -10550,6 +12206,7 @@ const QUICK_FILTER_ACTIVE_STYLE = {
|
|
|
10550
12206
|
};
|
|
10551
12207
|
const TASK_PANEL_STYLE = {
|
|
10552
12208
|
...panelStyle,
|
|
12209
|
+
position: "relative",
|
|
10553
12210
|
height: "100%",
|
|
10554
12211
|
minHeight: 0,
|
|
10555
12212
|
boxSizing: "border-box"
|
|
@@ -10563,7 +12220,7 @@ const SCROLL_BODY_STYLE = {
|
|
|
10563
12220
|
overflowY: "auto",
|
|
10564
12221
|
overflowX: "hidden"
|
|
10565
12222
|
};
|
|
10566
|
-
/**
|
|
12223
|
+
/** 排序(含本页全选 / 取消全选)与翻页同一行:操作靠左,翻页(含总篇数)靠右。 */
|
|
10567
12224
|
const PAGE_TOOLBAR_ROW_STYLE = {
|
|
10568
12225
|
display: "flex",
|
|
10569
12226
|
alignItems: "center",
|
|
@@ -10572,6 +12229,27 @@ const PAGE_TOOLBAR_ROW_STYLE = {
|
|
|
10572
12229
|
gap: 8,
|
|
10573
12230
|
padding: "2px 0"
|
|
10574
12231
|
};
|
|
12232
|
+
const PAGE_TOOLBAR_LEFT_STYLE = {
|
|
12233
|
+
display: "flex",
|
|
12234
|
+
alignItems: "center",
|
|
12235
|
+
flexWrap: "wrap",
|
|
12236
|
+
gap: 8
|
|
12237
|
+
};
|
|
12238
|
+
const PAGE_TOOLBAR_ACTION_STYLE = {
|
|
12239
|
+
...linkButtonStyle,
|
|
12240
|
+
fontSize: 11.5,
|
|
12241
|
+
whiteSpace: "nowrap"
|
|
12242
|
+
};
|
|
12243
|
+
const PAGE_TOOLBAR_ACTION_DISABLED_STYLE = {
|
|
12244
|
+
...PAGE_TOOLBAR_ACTION_STYLE,
|
|
12245
|
+
opacity: .4,
|
|
12246
|
+
cursor: "default"
|
|
12247
|
+
};
|
|
12248
|
+
const PAGE_TOOLBAR_COUNT_STYLE = {
|
|
12249
|
+
fontSize: 11.5,
|
|
12250
|
+
opacity: .66,
|
|
12251
|
+
whiteSpace: "nowrap"
|
|
12252
|
+
};
|
|
10575
12253
|
const PAGE_SORT_LABEL_STYLE = {
|
|
10576
12254
|
display: "flex",
|
|
10577
12255
|
alignItems: "center",
|
|
@@ -10636,6 +12314,12 @@ const PAPER_TAB_ID = "litsearch-paper";
|
|
|
10636
12314
|
/** 设置对话框里这一页的 section id。 */
|
|
10637
12315
|
const SETTINGS_SECTION_ID = "litsearch";
|
|
10638
12316
|
const HEALTH_REFRESH_MS = 3e4;
|
|
12317
|
+
/** 文件资源地址:与 DSH 文档预览 tab 使用的 `dsh-resource://file/session/...` 保持一致。 */
|
|
12318
|
+
function localFileAddress(sessionId, path) {
|
|
12319
|
+
const encode = (segment) => encodeURIComponent(segment).replace(/%3A/gi, ":");
|
|
12320
|
+
const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
|
|
12321
|
+
return `dsh-resource://file/session/${encode(sessionId)}/${normalized.split("/").map(encode).join("/")}`;
|
|
12322
|
+
}
|
|
10639
12323
|
const name = "dsh-plugin-litsearch";
|
|
10640
12324
|
const inject = [
|
|
10641
12325
|
"slots",
|
|
@@ -10710,13 +12394,33 @@ function apply(ctx) {
|
|
|
10710
12394
|
const address = `${PAPER_ADDRESS_PREFIX}${encodeURIComponent(taskId)}/${encodeURIComponent(paperId)}`;
|
|
10711
12395
|
ctx.sidebarRight.openResource(address, title === "" ? void 0 : { params: { title } });
|
|
10712
12396
|
};
|
|
12397
|
+
/**
|
|
12398
|
+
* 在右侧栏用 DSH 文档预览打开已下载的本地 PDF。
|
|
12399
|
+
* 页面加载后文件可能被删除,先让 Host 复核一次;缺失时状态会广播为
|
|
12400
|
+
* `missing`,按钮随即变成「下载文件已删除点击重新下载」。
|
|
12401
|
+
*/
|
|
12402
|
+
const openPdfPreview = (taskId, paperId, path) => {
|
|
12403
|
+
recheckPaperPdf(taskId, paperId).then((result) => result.exists).catch(() => true).then((exists) => {
|
|
12404
|
+
if (!exists || path === "") return;
|
|
12405
|
+
const sessionId = ctx.sidebarRight.mounted.getSnapshot();
|
|
12406
|
+
if (sessionId === void 0) return;
|
|
12407
|
+
ctx.sidebarRight.openResource(localFileAddress(sessionId, path), { revealIfOpened: true });
|
|
12408
|
+
});
|
|
12409
|
+
};
|
|
12410
|
+
/** 在系统文件管理器中打开下载目录并选中该 PDF(失败时静默,避免打断阅读)。 */
|
|
12411
|
+
const revealPdf = (path) => {
|
|
12412
|
+
if (path === "") return;
|
|
12413
|
+
revealLocalFile(path).catch(() => void 0);
|
|
12414
|
+
};
|
|
10713
12415
|
const TaskBody = (props) => (0, react.createElement)(TaskPane, {
|
|
10714
12416
|
t,
|
|
10715
12417
|
useTabInfo: props.useTabInfo,
|
|
10716
12418
|
inputActions: props.inputActions,
|
|
10717
12419
|
onOpenPaper: openPaper,
|
|
10718
12420
|
onOpenTask: (id, title) => openTask(id, title),
|
|
10719
|
-
onCloseTask: (id) => closeTask(id)
|
|
12421
|
+
onCloseTask: (id) => closeTask(id),
|
|
12422
|
+
onOpenLocal: openPdfPreview,
|
|
12423
|
+
onRevealLocal: revealPdf
|
|
10720
12424
|
});
|
|
10721
12425
|
/** tab 标题:按地址里的任务 id 读缓存,缺了补取一次详情。 */
|
|
10722
12426
|
const TaskTitle = (props) => {
|
|
@@ -10750,7 +12454,9 @@ function apply(ctx) {
|
|
|
10750
12454
|
const PaperBody = (props) => (0, react.createElement)(PaperPane, {
|
|
10751
12455
|
t,
|
|
10752
12456
|
useTabInfo: props.useTabInfo,
|
|
10753
|
-
inputActions: props.inputActions
|
|
12457
|
+
inputActions: props.inputActions,
|
|
12458
|
+
onOpenLocal: openPdfPreview,
|
|
12459
|
+
onRevealLocal: revealPdf
|
|
10754
12460
|
});
|
|
10755
12461
|
const PaperTitle = (props) => {
|
|
10756
12462
|
const params = props.useTabInfo().tab.navigation?.params;
|