@musnows/scriverse 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +831 -90
- package/dist/ai.js.map +1 -1
- package/dist/app.js +124 -58
- package/dist/app.js.map +1 -1
- package/dist/collaboration-presence.js +18 -7
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +67 -0
- package/dist/database.js.map +1 -1
- package/dist/public/ai-usage.d.ts +20 -0
- package/dist/public/ai-usage.js +75 -0
- package/dist/public/app.js +1089 -111
- package/dist/public/background-task-center.d.ts +28 -0
- package/dist/public/background-task-center.js +30 -0
- package/dist/public/index.html +41 -4
- package/dist/public/page-route.js +2 -2
- package/dist/public/race-hierarchy.js +35 -0
- package/dist/public/styles.css +230 -10
- package/dist/public/theme-init.js +2 -2
- package/dist/relationship-search.js +8 -1
- package/dist/relationship-search.js.map +1 -1
- package/dist/store.js +135 -75
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +7 -2
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineN
|
|
|
7
7
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
8
8
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
9
9
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
10
|
+
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
10
11
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
11
12
|
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-layered-context";
|
|
12
13
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
@@ -33,10 +34,10 @@ import {
|
|
|
33
34
|
timelineStatusLabel,
|
|
34
35
|
characterStateFieldLabel
|
|
35
36
|
} from "/display-labels.js?v=20260726-anthropic-messages-v2";
|
|
36
|
-
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=
|
|
37
|
+
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260727-ai-usage";
|
|
37
38
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
38
39
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
39
|
-
import {
|
|
40
|
+
import { eligibleRaceParents, orderRaceFilterOptions, paginateRaceForest, racePathLabel } from "/race-hierarchy.js?v=20260727-race-tree-pagination-v1";
|
|
40
41
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
41
42
|
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
42
43
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
@@ -44,6 +45,7 @@ import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global
|
|
|
44
45
|
import { resolveGlobalSearchTarget } from "/global-search.js?v=20260726-search-result-details";
|
|
45
46
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
46
47
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
48
|
+
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
47
49
|
import {
|
|
48
50
|
clampCropRect,
|
|
49
51
|
containImageRect,
|
|
@@ -56,7 +58,14 @@ import {
|
|
|
56
58
|
} from "/avatar-crop.js?v=20260725-avatar-crop";
|
|
57
59
|
|
|
58
60
|
const defaultPageSizes = Object.freeze({
|
|
61
|
+
settings: 30,
|
|
59
62
|
characters: 30,
|
|
63
|
+
races: 30,
|
|
64
|
+
organizations: 30,
|
|
65
|
+
timeline: 30,
|
|
66
|
+
outlines: 30,
|
|
67
|
+
relationships: 30,
|
|
68
|
+
reviews: 30,
|
|
60
69
|
analysisTasks: 30,
|
|
61
70
|
fileVersions: 30
|
|
62
71
|
});
|
|
@@ -122,11 +131,18 @@ let presenceHeartbeatQueued = null;
|
|
|
122
131
|
let presenceHeartbeatRequest = 0;
|
|
123
132
|
const acknowledgedCollaborativeChangeIds = new Set();
|
|
124
133
|
let collaborativeChangePromptOpen = false;
|
|
125
|
-
let
|
|
134
|
+
let relationshipPresenceId = null;
|
|
126
135
|
let collaborationAutoSaveDisabled = false;
|
|
127
136
|
|
|
128
137
|
let timelineMultiSelectEnabled = false;
|
|
129
138
|
let taskProgressRefreshTimer = null;
|
|
139
|
+
let relationshipSearchIndexRefreshTimer = null;
|
|
140
|
+
let backgroundTaskCenterTimer = null;
|
|
141
|
+
let backgroundTaskCenterRequest = 0;
|
|
142
|
+
let backgroundTaskCenterWorkId = null;
|
|
143
|
+
let backgroundTaskCenterTasksInitialized = false;
|
|
144
|
+
let backgroundTaskCenterTaskSnapshots = new Map();
|
|
145
|
+
let backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
130
146
|
const taskProgressRefreshInterval = 2_500;
|
|
131
147
|
const taskStatusSnapshots = new Map();
|
|
132
148
|
|
|
@@ -168,6 +184,10 @@ function analysisTaskStatusLabel(status) {
|
|
|
168
184
|
})[String(status)] ?? "未知状态";
|
|
169
185
|
}
|
|
170
186
|
|
|
187
|
+
function canRerunAnalysisTask(task) {
|
|
188
|
+
return ["review", "completed", "partial", "expired", "cancelled"].includes(String(task?.status ?? ""));
|
|
189
|
+
}
|
|
190
|
+
|
|
171
191
|
function normalizedAnalysisTaskStatus(status) {
|
|
172
192
|
const value = String(status);
|
|
173
193
|
return ["pending", "running", "review", "completed", "partial", "expired", "cancelled"].includes(value)
|
|
@@ -265,6 +285,7 @@ function applyWorkAccessMode() {
|
|
|
265
285
|
$("#ai-prompt").readOnly = aiReadOnly;
|
|
266
286
|
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
267
287
|
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
288
|
+
updateBackgroundTaskCenterVisibility();
|
|
268
289
|
if (proseReadOnly) {
|
|
269
290
|
chapterEditorReadOnly = true;
|
|
270
291
|
cancelChapterAutoSave();
|
|
@@ -338,6 +359,7 @@ let workScopedUiGeneration = 0;
|
|
|
338
359
|
let importHistoryRecords = [];
|
|
339
360
|
let importHistoryNextPage = null;
|
|
340
361
|
let importHistoryRequestId = 0;
|
|
362
|
+
let chapterInsightRequestId = 0;
|
|
341
363
|
|
|
342
364
|
const shelfOnboardingSteps = [
|
|
343
365
|
{ selector: "#home-button", eyebrow: "作品入口", title: "这里是你的创作书架", description: "点击左上角的叙界标志,可以随时回到书架,在不同作品之间切换。", placement: "bottom" },
|
|
@@ -538,11 +560,12 @@ function replacePageRoute(route) {
|
|
|
538
560
|
}
|
|
539
561
|
|
|
540
562
|
function presencePageForRoute(route = currentPageRoute()) {
|
|
541
|
-
if (!state.work || route.view === "shelf" || route.view === "platform-ai") return null;
|
|
563
|
+
if (!state.work || route.view === "shelf" || route.view === "platform-ai" || route.view === "platform-usage") return null;
|
|
564
|
+
if (relationshipPresenceId) return { kind: "entity-editor", module: "relationship", resourceId: relationshipPresenceId };
|
|
542
565
|
if (route.view === "editor") return { kind: "editor", resourceId: String(route.chapterId ?? "") || undefined };
|
|
543
566
|
if (route.view === "entity-editor") return { kind: "entity-editor", module: route.entity, resourceId: String(route.entityId ?? "") || undefined };
|
|
544
567
|
if (route.view === "module") return { kind: "module", module: route.module };
|
|
545
|
-
if (route.view === "settings" || route.view === "platform-ai") return { kind: "settings" };
|
|
568
|
+
if (route.view === "settings" || route.view === "platform-ai" || route.view === "platform-usage") return { kind: "settings" };
|
|
546
569
|
return { kind: "welcome" };
|
|
547
570
|
}
|
|
548
571
|
|
|
@@ -628,7 +651,7 @@ async function refreshPresence() {
|
|
|
628
651
|
presenceParticipants = Array.isArray(payload) ? payload : (payload?.participants ?? []);
|
|
629
652
|
renderPresence();
|
|
630
653
|
const recentChanges = Array.isArray(payload) ? [] : (payload?.recentChanges ?? []);
|
|
631
|
-
void
|
|
654
|
+
void handleRelationshipCollaborativeChanges(recentChanges);
|
|
632
655
|
}
|
|
633
656
|
} catch {
|
|
634
657
|
if (state.work?.id === workId) renderPresence();
|
|
@@ -638,14 +661,10 @@ async function refreshPresence() {
|
|
|
638
661
|
return presenceParticipants;
|
|
639
662
|
}
|
|
640
663
|
|
|
641
|
-
function
|
|
642
|
-
return Boolean(state.dirty || entityEditorDirty || characterSectionEditorDirty || knowledgeSectionEditorDirty);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async function handleCollaborativeChanges(recentChanges) {
|
|
664
|
+
async function handleRelationshipCollaborativeChanges(recentChanges) {
|
|
646
665
|
if (!Array.isArray(recentChanges) || !recentChanges.length || collaborativeChangePromptOpen) return;
|
|
647
666
|
const localKey = presencePageKey(presencePageForRoute());
|
|
648
|
-
if (!localKey) return;
|
|
667
|
+
if (!localKey.startsWith("entity-editor:relationship:")) return;
|
|
649
668
|
const selfId = state.user?.userId;
|
|
650
669
|
const incoming = recentChanges.filter((change) => (
|
|
651
670
|
change
|
|
@@ -662,14 +681,10 @@ async function handleCollaborativeChanges(recentChanges) {
|
|
|
662
681
|
const oldest = acknowledgedCollaborativeChangeIds.values().next().value;
|
|
663
682
|
acknowledgedCollaborativeChangeIds.delete(oldest);
|
|
664
683
|
}
|
|
665
|
-
peerPageStale = true;
|
|
666
684
|
collaborativeChangePromptOpen = true;
|
|
667
|
-
const dirtyHint = localEditingDirty()
|
|
668
|
-
? "你当前页面还有未保存修改。请先把正在编辑的内容复制保存到别处(本页无法代为另存),再刷新页面后重新提交。"
|
|
669
|
-
: "请先确认本地没有需要保留的草稿;如有,请复制保存到别处(本页无法代为另存),再刷新页面后继续编辑。";
|
|
670
685
|
try {
|
|
671
|
-
const shouldReload = await confirmToast(`${latest.actorDisplayName || "协作者"}
|
|
672
|
-
title: "
|
|
686
|
+
const shouldReload = await confirmToast(`${latest.actorDisplayName || "协作者"}已更新当前人物关系。请先确认本地没有需要保留的修改,再刷新页面继续查看。`, {
|
|
687
|
+
title: "人物关系已更新",
|
|
673
688
|
confirmLabel: "刷新页面",
|
|
674
689
|
cancelLabel: "稍后处理"
|
|
675
690
|
});
|
|
@@ -679,17 +694,6 @@ async function handleCollaborativeChanges(recentChanges) {
|
|
|
679
694
|
}
|
|
680
695
|
}
|
|
681
696
|
|
|
682
|
-
async function confirmPeerStaleSave() {
|
|
683
|
-
if (!peerPageStale) return true;
|
|
684
|
-
const confirmed = await confirmToast("协作者已更新当前页面。请先把正在编辑的内容复制保存到别处(本页无法代为另存),再刷新页面后重新提交。继续保存可能覆盖对方修改或触发冲突。", {
|
|
685
|
-
title: "页面内容已过期",
|
|
686
|
-
confirmLabel: "仍然保存",
|
|
687
|
-
cancelLabel: "暂不保存"
|
|
688
|
-
});
|
|
689
|
-
if (confirmed) peerPageStale = false;
|
|
690
|
-
return confirmed;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
697
|
function schedulePresenceHeartbeat() {
|
|
694
698
|
if (presenceHeartbeatQueued !== null) clearTimeout(presenceHeartbeatQueued);
|
|
695
699
|
presenceHeartbeatQueued = setTimeout(() => {
|
|
@@ -698,9 +702,15 @@ function schedulePresenceHeartbeat() {
|
|
|
698
702
|
}, 80);
|
|
699
703
|
}
|
|
700
704
|
|
|
705
|
+
function setRelationshipPresence(relationshipId) {
|
|
706
|
+
const nextId = relationshipId ? String(relationshipId) : null;
|
|
707
|
+
if (relationshipPresenceId === nextId) return;
|
|
708
|
+
relationshipPresenceId = nextId;
|
|
709
|
+
void refreshPresence();
|
|
710
|
+
}
|
|
711
|
+
|
|
701
712
|
async function confirmConcurrentSave() {
|
|
702
713
|
await refreshPresence();
|
|
703
|
-
if (!(await confirmPeerStaleSave())) return false;
|
|
704
714
|
const localKey = presencePageKey(presencePageForRoute());
|
|
705
715
|
const peers = presenceParticipants.filter((participant) => participant.clientId !== presenceClientId && participant.page.key === localKey);
|
|
706
716
|
if (!peers.length) return true;
|
|
@@ -720,6 +730,7 @@ function currentPageRoute() {
|
|
|
720
730
|
}
|
|
721
731
|
if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
|
|
722
732
|
if (!$("#platform-ai-view").classList.contains("hidden")) return { view: "platform-ai", workId, ...settingsRouteContext() };
|
|
733
|
+
if (!$("#platform-usage-view").classList.contains("hidden")) return { view: "platform-usage", workId, ...settingsRouteContext() };
|
|
723
734
|
if (!$("#shelf-view").classList.contains("hidden")) return { view: "shelf" };
|
|
724
735
|
if (!workId) return { view: "shelf" };
|
|
725
736
|
if (!$("#editor-view").classList.contains("hidden")) return { view: "editor", workId, chapterId: state.chapter?.id ?? null };
|
|
@@ -843,6 +854,16 @@ let entityEditorReadOnly = false;
|
|
|
843
854
|
let chapterEditorReadOnly = true;
|
|
844
855
|
let characterListPage = 1;
|
|
845
856
|
let taskListPage = 1;
|
|
857
|
+
const moduleListPages = {
|
|
858
|
+
settings: 1,
|
|
859
|
+
races: 1,
|
|
860
|
+
organizations: 1,
|
|
861
|
+
timeline: 1,
|
|
862
|
+
outlinePlans: 1,
|
|
863
|
+
foreshadows: 1,
|
|
864
|
+
relationships: 1,
|
|
865
|
+
reviews: 1
|
|
866
|
+
};
|
|
846
867
|
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
847
868
|
let characterFiltersPanelOpen = false;
|
|
848
869
|
const relationshipFilters = { fromCharacterIds: [], toCharacterIds: [] };
|
|
@@ -2077,6 +2098,40 @@ function pageSizeFor(module) {
|
|
|
2077
2098
|
return normalizePageSize(state.uiSettings.pageSizes[module], defaultPageSizes[module] ?? 30);
|
|
2078
2099
|
}
|
|
2079
2100
|
|
|
2101
|
+
function paginateModuleItems(items, page, sizeKey) {
|
|
2102
|
+
const limit = pageSizeFor(sizeKey);
|
|
2103
|
+
const total = items.length;
|
|
2104
|
+
const pageCount = Math.max(1, Math.ceil(total / limit));
|
|
2105
|
+
const safePage = Math.min(Math.max(1, Number(page) || 1), pageCount);
|
|
2106
|
+
const start = (safePage - 1) * limit;
|
|
2107
|
+
return {
|
|
2108
|
+
items: items.slice(start, start + limit),
|
|
2109
|
+
page: safePage,
|
|
2110
|
+
limit,
|
|
2111
|
+
total,
|
|
2112
|
+
pageCount,
|
|
2113
|
+
hasMore: safePage < pageCount,
|
|
2114
|
+
nextPage: safePage < pageCount ? safePage + 1 : null
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function renderModulePagination(pageResult, pageKey, label) {
|
|
2119
|
+
if (pageResult.pageCount <= 1) return "";
|
|
2120
|
+
return `<nav class="module-pagination" aria-label="${esc(label)}分页">
|
|
2121
|
+
<button type="button" data-module-page-key="${esc(pageKey)}" data-module-page="${pageResult.page - 1}" ${pageResult.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
2122
|
+
<span>第 ${pageResult.page}/${pageResult.pageCount} 页 · 本页 ${pageResult.itemCount ?? pageResult.items.length} 条 · 共 ${pageResult.total} 条</span>
|
|
2123
|
+
<button type="button" data-module-page-key="${esc(pageKey)}" data-module-page="${pageResult.nextPage ?? pageResult.page + 1}" ${pageResult.hasMore ? "" : "disabled"}>下一页</button>
|
|
2124
|
+
</nav>`;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function bindModulePagination(pageKey, renderPage) {
|
|
2128
|
+
$("#module-content").querySelectorAll(`[data-module-page-key="${CSS.escape(pageKey)}"]`).forEach((button) => button.addEventListener("click", async () => {
|
|
2129
|
+
if (button.disabled) return;
|
|
2130
|
+
$("#module-content").querySelectorAll(`[data-module-page-key="${CSS.escape(pageKey)}"]`).forEach((control) => { control.disabled = true; });
|
|
2131
|
+
await renderPage(Number(button.dataset.modulePage));
|
|
2132
|
+
}));
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2080
2135
|
async function loadPlatformUiSettings() {
|
|
2081
2136
|
try {
|
|
2082
2137
|
applyPlatformUiSettings(await api("/api/ui-settings"));
|
|
@@ -2110,6 +2165,16 @@ function raiseToastRegion() {
|
|
|
2110
2165
|
region.showPopover();
|
|
2111
2166
|
}
|
|
2112
2167
|
|
|
2168
|
+
function dismissChapterInsightToast() {
|
|
2169
|
+
chapterInsightRequestId += 1;
|
|
2170
|
+
const region = $("#toast-region");
|
|
2171
|
+
region.querySelector(".chapter-insight-toast")?.remove();
|
|
2172
|
+
$("#insight-button").setAttribute("aria-expanded", "false");
|
|
2173
|
+
if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
|
|
2174
|
+
region.hidePopover();
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2113
2178
|
function toast(message, type = "info") {
|
|
2114
2179
|
const region = $("#toast-region");
|
|
2115
2180
|
const element = document.createElement("div");
|
|
@@ -2467,6 +2532,11 @@ async function initializePage() {
|
|
|
2467
2532
|
if (route.view === "platform-ai") {
|
|
2468
2533
|
await showPlatformAi();
|
|
2469
2534
|
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (route.view === "platform-usage") {
|
|
2538
|
+
await showPlatformUsage();
|
|
2539
|
+
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
2470
2540
|
}
|
|
2471
2541
|
} finally {
|
|
2472
2542
|
document.body.classList.remove("auth-pending");
|
|
@@ -2477,12 +2547,15 @@ async function initializePage() {
|
|
|
2477
2547
|
}
|
|
2478
2548
|
|
|
2479
2549
|
function showShelf() {
|
|
2550
|
+
stopBackgroundTaskCenter();
|
|
2551
|
+
dismissChapterInsightToast();
|
|
2480
2552
|
state.dirty = false;
|
|
2481
2553
|
settingsReturnContext = null;
|
|
2482
2554
|
updateDocumentTitle();
|
|
2483
2555
|
$("#app").classList.add("shelf-mode");
|
|
2484
2556
|
$("#shelf-view").classList.remove("hidden");
|
|
2485
2557
|
$("#platform-ai-view").classList.add("hidden");
|
|
2558
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2486
2559
|
$("#settings-hub-view").classList.add("hidden");
|
|
2487
2560
|
$("#welcome-view").classList.add("hidden");
|
|
2488
2561
|
$("#editor-view").classList.add("hidden");
|
|
@@ -2509,6 +2582,7 @@ function renderSettingsHub() {
|
|
|
2509
2582
|
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
2510
2583
|
const isAdmin = state.user?.role === "admin";
|
|
2511
2584
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
2585
|
+
$("#platform-usage-button").classList.toggle("hidden", !isAdmin);
|
|
2512
2586
|
$("#user-management-button").classList.toggle("hidden", !isAdmin);
|
|
2513
2587
|
$("#platform-ui-settings-button").classList.toggle("hidden", !isAdmin);
|
|
2514
2588
|
$("#collaboration-button").disabled = !canManageWork;
|
|
@@ -2570,7 +2644,14 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2570
2644
|
const settings = await api("/api/platform/ui-settings");
|
|
2571
2645
|
$("#toast-position").value = settings.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2572
2646
|
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
2647
|
+
$("#page-size-settings").value = String(pageSizes.settings);
|
|
2573
2648
|
$("#page-size-characters").value = String(pageSizes.characters);
|
|
2649
|
+
$("#page-size-races").value = String(pageSizes.races);
|
|
2650
|
+
$("#page-size-organizations").value = String(pageSizes.organizations);
|
|
2651
|
+
$("#page-size-timeline").value = String(pageSizes.timeline);
|
|
2652
|
+
$("#page-size-outlines").value = String(pageSizes.outlines);
|
|
2653
|
+
$("#page-size-relationships").value = String(pageSizes.relationships);
|
|
2654
|
+
$("#page-size-reviews").value = String(pageSizes.reviews);
|
|
2574
2655
|
$("#page-size-analysis-tasks").value = String(pageSizes.analysisTasks);
|
|
2575
2656
|
$("#page-size-file-versions").value = String(pageSizes.fileVersions);
|
|
2576
2657
|
$("#platform-ui-settings-dialog").showModal();
|
|
@@ -2727,7 +2808,9 @@ async function openSearchResult(result) {
|
|
|
2727
2808
|
const target = resolveGlobalSearchTarget(result);
|
|
2728
2809
|
if (!target) throw new Error("无法打开该搜索结果");
|
|
2729
2810
|
$("#search-dialog").close();
|
|
2730
|
-
const inSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2811
|
+
const inSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2812
|
+
|| !$("#platform-ai-view").classList.contains("hidden")
|
|
2813
|
+
|| !$("#platform-usage-view").classList.contains("hidden");
|
|
2731
2814
|
if (inSettings) await returnFromSettings();
|
|
2732
2815
|
if (target.kind === "chapter") {
|
|
2733
2816
|
await selectChapter(target.id);
|
|
@@ -2743,16 +2826,20 @@ async function openSearchResult(result) {
|
|
|
2743
2826
|
}
|
|
2744
2827
|
|
|
2745
2828
|
async function showSettingsHub() {
|
|
2746
|
-
const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2829
|
+
const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden")
|
|
2830
|
+
|| !$("#platform-ai-view").classList.contains("hidden")
|
|
2831
|
+
|| !$("#platform-usage-view").classList.contains("hidden");
|
|
2747
2832
|
if (!alreadyInSettings) {
|
|
2748
2833
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?"))) return false;
|
|
2749
2834
|
settingsReturnContext = captureSettingsReturnContext();
|
|
2750
2835
|
state.dirty = false;
|
|
2751
2836
|
}
|
|
2837
|
+
dismissChapterInsightToast();
|
|
2752
2838
|
updateDocumentTitle(state.work);
|
|
2753
2839
|
$("#app").classList.add("shelf-mode");
|
|
2754
2840
|
$("#shelf-view").classList.add("hidden");
|
|
2755
2841
|
$("#platform-ai-view").classList.add("hidden");
|
|
2842
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2756
2843
|
$("#settings-hub-view").classList.remove("hidden");
|
|
2757
2844
|
$("#welcome-view").classList.add("hidden");
|
|
2758
2845
|
$("#editor-view").classList.add("hidden");
|
|
@@ -2778,6 +2865,7 @@ async function returnFromSettings() {
|
|
|
2778
2865
|
$("#settings-button").removeAttribute("aria-current");
|
|
2779
2866
|
$("#settings-hub-view").classList.add("hidden");
|
|
2780
2867
|
$("#platform-ai-view").classList.add("hidden");
|
|
2868
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2781
2869
|
if (context.view === "shelf" || !state.work) return showShelf();
|
|
2782
2870
|
$("#app").classList.remove("shelf-mode");
|
|
2783
2871
|
$("#shelf-view").classList.add("hidden");
|
|
@@ -2791,10 +2879,12 @@ async function returnFromSettings() {
|
|
|
2791
2879
|
async function showPlatformAi() {
|
|
2792
2880
|
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
|
|
2793
2881
|
state.dirty = false;
|
|
2882
|
+
dismissChapterInsightToast();
|
|
2794
2883
|
updateDocumentTitle();
|
|
2795
2884
|
$("#app").classList.add("shelf-mode");
|
|
2796
2885
|
$("#shelf-view").classList.add("hidden");
|
|
2797
2886
|
$("#platform-ai-view").classList.remove("hidden");
|
|
2887
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2798
2888
|
$("#settings-hub-view").classList.add("hidden");
|
|
2799
2889
|
$("#welcome-view").classList.add("hidden");
|
|
2800
2890
|
$("#editor-view").classList.add("hidden");
|
|
@@ -2807,6 +2897,26 @@ async function showPlatformAi() {
|
|
|
2807
2897
|
return true;
|
|
2808
2898
|
}
|
|
2809
2899
|
|
|
2900
|
+
async function showPlatformUsage() {
|
|
2901
|
+
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入 Token 用量面板将放弃本地修改。是否继续?"))) return false;
|
|
2902
|
+
state.dirty = false;
|
|
2903
|
+
updateDocumentTitle();
|
|
2904
|
+
$("#app").classList.add("shelf-mode");
|
|
2905
|
+
$("#shelf-view").classList.add("hidden");
|
|
2906
|
+
$("#platform-ai-view").classList.add("hidden");
|
|
2907
|
+
$("#platform-usage-view").classList.remove("hidden");
|
|
2908
|
+
$("#settings-hub-view").classList.add("hidden");
|
|
2909
|
+
$("#welcome-view").classList.add("hidden");
|
|
2910
|
+
$("#editor-view").classList.add("hidden");
|
|
2911
|
+
$("#module-view").classList.add("hidden");
|
|
2912
|
+
$("#work-meta").textContent = "Token 用量";
|
|
2913
|
+
$("#settings-button").setAttribute("aria-current", "page");
|
|
2914
|
+
setSaveState("Token 用量");
|
|
2915
|
+
await renderPlatformTokenUsage();
|
|
2916
|
+
replacePageRoute({ view: "platform-usage", workId: state.work?.id ?? null, ...settingsRouteContext() });
|
|
2917
|
+
return true;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2810
2920
|
function renderShelf() {
|
|
2811
2921
|
const shelf = $("#book-shelf");
|
|
2812
2922
|
shelf.innerHTML = `${state.works.map((work) => `
|
|
@@ -2830,6 +2940,7 @@ function renderShelf() {
|
|
|
2830
2940
|
}
|
|
2831
2941
|
|
|
2832
2942
|
function resetWorkScopedUiCaches() {
|
|
2943
|
+
stopBackgroundTaskCenter();
|
|
2833
2944
|
workScopedUiGeneration += 1;
|
|
2834
2945
|
loadedAiModelsWorkId = null;
|
|
2835
2946
|
loadedAiReferencesWorkId = null;
|
|
@@ -2844,6 +2955,7 @@ function resetWorkScopedUiCaches() {
|
|
|
2844
2955
|
state.characters = [];
|
|
2845
2956
|
state.settings = [];
|
|
2846
2957
|
characterListPage = 1;
|
|
2958
|
+
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
2847
2959
|
relationshipFilters.fromCharacterIds = [];
|
|
2848
2960
|
relationshipFilters.toCharacterIds = [];
|
|
2849
2961
|
taskListPage = 1;
|
|
@@ -2877,6 +2989,7 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2877
2989
|
$("#app").classList.remove("shelf-mode");
|
|
2878
2990
|
$("#shelf-view").classList.add("hidden");
|
|
2879
2991
|
$("#platform-ai-view").classList.add("hidden");
|
|
2992
|
+
$("#platform-usage-view").classList.add("hidden");
|
|
2880
2993
|
$("#settings-hub-view").classList.add("hidden");
|
|
2881
2994
|
$("#settings-button").removeAttribute("aria-current");
|
|
2882
2995
|
settingsReturnContext = null;
|
|
@@ -2885,6 +2998,7 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2885
2998
|
chapterEditorReadOnly = true;
|
|
2886
2999
|
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
2887
3000
|
applyWorkAccessMode();
|
|
3001
|
+
startBackgroundTaskCenter(nextWork.id);
|
|
2888
3002
|
updateDocumentTitle(state.work);
|
|
2889
3003
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
2890
3004
|
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
@@ -2993,7 +3107,7 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
2993
3107
|
$("#chapter-content").value = normalizedContent;
|
|
2994
3108
|
clearChapterLineSelection();
|
|
2995
3109
|
scheduleChapterLineNumbers();
|
|
2996
|
-
|
|
3110
|
+
dismissChapterInsightToast();
|
|
2997
3111
|
updateChapterStats();
|
|
2998
3112
|
if (!canEditProse()) setSaveState("正文只读");
|
|
2999
3113
|
else if (chapterEditorReadOnly) setSaveState("阅读模式");
|
|
@@ -3028,6 +3142,7 @@ function tidyChapterBlankLines() {
|
|
|
3028
3142
|
}
|
|
3029
3143
|
|
|
3030
3144
|
function showWelcome(hasWork = false) {
|
|
3145
|
+
dismissChapterInsightToast();
|
|
3031
3146
|
$("#editor-view").classList.add("hidden");
|
|
3032
3147
|
$("#module-view").classList.add("hidden");
|
|
3033
3148
|
$("#welcome-view").classList.remove("hidden");
|
|
@@ -3078,6 +3193,7 @@ async function showModule(module) {
|
|
|
3078
3193
|
}
|
|
3079
3194
|
return;
|
|
3080
3195
|
}
|
|
3196
|
+
dismissChapterInsightToast();
|
|
3081
3197
|
$("#welcome-view").classList.add("hidden");
|
|
3082
3198
|
$("#editor-view").classList.add("hidden");
|
|
3083
3199
|
$("#module-view").classList.remove("hidden");
|
|
@@ -3508,19 +3624,22 @@ function renderSettingRows(records) {
|
|
|
3508
3624
|
}).join("")}</div>`;
|
|
3509
3625
|
}
|
|
3510
3626
|
|
|
3511
|
-
async function renderSettings() {
|
|
3627
|
+
async function renderSettings(page = moduleListPages.settings) {
|
|
3512
3628
|
const records = await apiAllPages(`/api/works/${state.work.id}/settings`);
|
|
3513
3629
|
state.settings = records;
|
|
3514
3630
|
mountModuleCount(records.length);
|
|
3631
|
+
const pageResult = paginateModuleItems(records, page, "settings");
|
|
3632
|
+
moduleListPages.settings = pageResult.page;
|
|
3515
3633
|
const layout = readModuleLayout();
|
|
3516
3634
|
if (records.length) mountModuleLayoutToggle(layout, "设定列表样式");
|
|
3517
3635
|
$("#module-content").innerHTML = records.length
|
|
3518
|
-
? `${layout === "rows" ? renderSettingRows(
|
|
3636
|
+
? `${layout === "rows" ? renderSettingRows(pageResult.items) : renderSettingCards(pageResult.items)}${renderModulePagination(pageResult, "settings", "设定库")}`
|
|
3519
3637
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
3520
|
-
bindModuleLayoutToggle(renderSettings);
|
|
3638
|
+
bindModuleLayoutToggle(() => renderSettings(pageResult.page));
|
|
3639
|
+
bindModulePagination("settings", renderSettings);
|
|
3521
3640
|
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
3522
3641
|
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
3523
|
-
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
3642
|
+
bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
|
|
3524
3643
|
}
|
|
3525
3644
|
|
|
3526
3645
|
async function renderCharacters(page = characterListPage) {
|
|
@@ -3536,14 +3655,15 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3536
3655
|
: characterSource;
|
|
3537
3656
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3538
3657
|
characterListPage = characterPage.page;
|
|
3539
|
-
|
|
3658
|
+
const pageCharacters = characterPage.items;
|
|
3659
|
+
[state.races, state.organizations] = [races, organizations];
|
|
3540
3660
|
mountModuleCount(characterPage.total);
|
|
3541
3661
|
const layout = readModuleLayout();
|
|
3542
3662
|
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
3543
3663
|
const characterLockBadge = (item) => item.lockedFields.length
|
|
3544
3664
|
? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
|
|
3545
3665
|
: "";
|
|
3546
|
-
const characterCards = () => `<div class="card-grid">${
|
|
3666
|
+
const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
|
|
3547
3667
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
3548
3668
|
return `
|
|
3549
3669
|
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
@@ -3558,7 +3678,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3558
3678
|
${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
|
|
3559
3679
|
</article>`;
|
|
3560
3680
|
}).join("")}</div>`;
|
|
3561
|
-
const characterRows = () => `<div class="module-row-list">${
|
|
3681
|
+
const characterRows = () => `<div class="module-row-list">${pageCharacters.map((item) => {
|
|
3562
3682
|
const preview = moduleRowPreview(item.profile?.summary || item.attributes?.identity || Object.entries(item.currentState).map(([key, value]) => `${characterStateFieldLabel(key)}:${value}`).join(" ") || "尚未记录当前状态");
|
|
3563
3683
|
const meta = [
|
|
3564
3684
|
item.code ? `编号 ${item.code}` : "",
|
|
@@ -3574,10 +3694,10 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3574
3694
|
<div class="card-actions">${characterActions(item)}</div>
|
|
3575
3695
|
</article>`;
|
|
3576
3696
|
}).join("")}</div>`;
|
|
3577
|
-
const pagination =
|
|
3697
|
+
const pagination = pageCharacters.length && (characterPage.page > 1 || characterPage.hasMore)
|
|
3578
3698
|
? `<nav class="module-pagination" aria-label="角色列表分页">
|
|
3579
3699
|
<button type="button" data-character-page="${characterPage.page - 1}" ${characterPage.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
3580
|
-
<span>第 ${characterPage.page}/${Math.ceil(characterPage.total / characterPage.limit)} 页 · 本页 ${
|
|
3700
|
+
<span>第 ${characterPage.page}/${Math.ceil(characterPage.total / characterPage.limit)} 页 · 本页 ${pageCharacters.length} 个角色 · 共 ${characterPage.total} 个角色</span>
|
|
3581
3701
|
<button type="button" data-character-page="${characterPage.nextPage ?? characterPage.page + 1}" ${characterPage.hasMore ? "" : "disabled"}>下一页</button>
|
|
3582
3702
|
</nav>`
|
|
3583
3703
|
: "";
|
|
@@ -3596,8 +3716,8 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3596
3716
|
<div class="character-filter-toolbar-actions">${hasCharacterFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${characterPage.total} 个角色</span>` : ""}<button id="clear-character-filters" class="ghost-button" type="button" ${hasCharacterFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
3597
3717
|
</section>`;
|
|
3598
3718
|
mountCharacterFilterToggle();
|
|
3599
|
-
if (
|
|
3600
|
-
$("#module-content").innerHTML = filterToolbar + (
|
|
3719
|
+
if (pageCharacters.length) mountModuleLayoutToggle(layout, "角色列表样式");
|
|
3720
|
+
$("#module-content").innerHTML = filterToolbar + (pageCharacters.length
|
|
3601
3721
|
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
3602
3722
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
3603
3723
|
bindModuleLayoutToggle(renderCharacters);
|
|
@@ -3626,14 +3746,18 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3626
3746
|
$("#module-content").querySelectorAll("[data-character-page]").forEach((control) => { control.disabled = true; });
|
|
3627
3747
|
await renderCharacters(Number(button.dataset.characterPage));
|
|
3628
3748
|
}));
|
|
3629
|
-
bindRecordPreview("[data-open-character]", (id) => openCharacterEditor(
|
|
3630
|
-
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(
|
|
3749
|
+
bindRecordPreview("[data-open-character]", (id) => openCharacterEditor(pageCharacters.find((item) => item.id === id), { readOnly: true }));
|
|
3750
|
+
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(pageCharacters.find((item) => item.id === button.dataset.editCharacter))));
|
|
3631
3751
|
}
|
|
3632
3752
|
|
|
3633
|
-
async function renderRaces() {
|
|
3753
|
+
async function renderRaces(page = moduleListPages.races) {
|
|
3634
3754
|
state.races = await apiAllPages(`/api/works/${state.work.id}/races`);
|
|
3635
3755
|
mountModuleCount(state.races.length);
|
|
3636
3756
|
const layout = readModuleLayout();
|
|
3757
|
+
const pageResult = layout === "rows"
|
|
3758
|
+
? paginateModuleItems(state.races, page, "races")
|
|
3759
|
+
: paginateRaceForest(state.races, page, pageSizeFor("races"));
|
|
3760
|
+
moduleListPages.races = pageResult.page;
|
|
3637
3761
|
const canEditRaces = canEditModule("races");
|
|
3638
3762
|
const raceActions = (item) => canEditRaces
|
|
3639
3763
|
? recordCardEditButton("edit-race", item.id, `种族“${item.name}”`)
|
|
@@ -3654,7 +3778,7 @@ async function renderRaces() {
|
|
|
3654
3778
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
3655
3779
|
</div>
|
|
3656
3780
|
</details>`;
|
|
3657
|
-
const raceRows = () => `<div class="module-row-list">${
|
|
3781
|
+
const raceRows = () => `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
3658
3782
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
3659
3783
|
const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
3660
3784
|
return `
|
|
@@ -3668,22 +3792,25 @@ async function renderRaces() {
|
|
|
3668
3792
|
if (state.races.length) mountModuleLayoutToggle(layout, "种族列表样式");
|
|
3669
3793
|
if (state.races.length && layout !== "rows") mountRaceTreeExpandToggle();
|
|
3670
3794
|
$("#module-content").innerHTML = state.races.length
|
|
3671
|
-
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${
|
|
3795
|
+
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${pageResult.items.map(renderRaceNode).join("")}</section>`}${renderModulePagination(pageResult, "races", "种族列表")}`
|
|
3672
3796
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
3673
|
-
bindModuleLayoutToggle(renderRaces);
|
|
3797
|
+
bindModuleLayoutToggle(() => renderRaces(pageResult.page));
|
|
3798
|
+
bindModulePagination("races", renderRaces);
|
|
3674
3799
|
bindRaceTreeExpandToggle();
|
|
3675
3800
|
bindRaceTreeNodeToggles();
|
|
3676
3801
|
const openRace = async (id, readOnly) => openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly });
|
|
3677
3802
|
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => { void openRace(button.dataset.editRace, false); }));
|
|
3678
|
-
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
3803
|
+
bindEntityHistoryButtons(async () => { await renderRaces(pageResult.page); await loadAiReferences(); });
|
|
3679
3804
|
}
|
|
3680
3805
|
|
|
3681
|
-
async function renderOrganizations() {
|
|
3806
|
+
async function renderOrganizations(page = moduleListPages.organizations) {
|
|
3682
3807
|
[state.organizations, state.characters] = await Promise.all([
|
|
3683
3808
|
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
3684
3809
|
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
3685
3810
|
]);
|
|
3686
3811
|
mountModuleCount(state.organizations.length);
|
|
3812
|
+
const pageResult = paginateModuleItems(state.organizations, page, "organizations");
|
|
3813
|
+
moduleListPages.organizations = pageResult.page;
|
|
3687
3814
|
const layout = readModuleLayout();
|
|
3688
3815
|
const canEditOrganizations = canEditModule("organizations");
|
|
3689
3816
|
const organizationActions = (item) => canEditOrganizations
|
|
@@ -3692,14 +3819,14 @@ async function renderOrganizations() {
|
|
|
3692
3819
|
const organizationCardActions = (item) => canEditOrganizations
|
|
3693
3820
|
? organizationActions(item)
|
|
3694
3821
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
3695
|
-
const organizationCards = () => `<div class="card-grid organization-grid">${
|
|
3822
|
+
const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
|
|
3696
3823
|
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
3697
3824
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
3698
3825
|
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
3699
3826
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3700
3827
|
${organizationCardActions(item)}
|
|
3701
3828
|
</article>`).join("")}</div>`;
|
|
3702
|
-
const organizationRows = () => `<div class="module-row-list">${
|
|
3829
|
+
const organizationRows = () => `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
3703
3830
|
const preview = moduleRowPreview(item.description || "尚未填写组织简介");
|
|
3704
3831
|
const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
|
|
3705
3832
|
return `
|
|
@@ -3712,12 +3839,13 @@ async function renderOrganizations() {
|
|
|
3712
3839
|
}).join("")}</div>`;
|
|
3713
3840
|
if (state.organizations.length) mountModuleLayoutToggle(layout, "组织列表样式");
|
|
3714
3841
|
$("#module-content").innerHTML = state.organizations.length
|
|
3715
|
-
? `${layout === "rows" ? organizationRows() : organizationCards()}`
|
|
3842
|
+
? `${layout === "rows" ? organizationRows() : organizationCards()}${renderModulePagination(pageResult, "organizations", "组织列表")}`
|
|
3716
3843
|
: emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
3717
|
-
bindModuleLayoutToggle(renderOrganizations);
|
|
3844
|
+
bindModuleLayoutToggle(() => renderOrganizations(pageResult.page));
|
|
3845
|
+
bindModulePagination("organizations", renderOrganizations);
|
|
3718
3846
|
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
3719
3847
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
3720
|
-
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
3848
|
+
bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
|
|
3721
3849
|
}
|
|
3722
3850
|
|
|
3723
3851
|
function updateTimelineMultiSelectControls() {
|
|
@@ -3742,12 +3870,14 @@ function setTimelineMultiSelectMode(enabled) {
|
|
|
3742
3870
|
updateTimelineMultiSelectControls();
|
|
3743
3871
|
}
|
|
3744
3872
|
|
|
3745
|
-
async function renderTimeline() {
|
|
3873
|
+
async function renderTimeline(page = moduleListPages.timeline) {
|
|
3746
3874
|
const [events, tracks] = await Promise.all([
|
|
3747
3875
|
apiAllPages(`/api/works/${state.work.id}/timeline`),
|
|
3748
3876
|
apiAllPages(`/api/works/${state.work.id}/timeline-tracks`)
|
|
3749
3877
|
]);
|
|
3750
3878
|
mountModuleCount(events.length);
|
|
3879
|
+
const pageResult = paginateModuleItems(events, page, "timeline");
|
|
3880
|
+
moduleListPages.timeline = pageResult.page;
|
|
3751
3881
|
timelineMultiSelectEnabled = false;
|
|
3752
3882
|
$("#timeline-tools")?.remove();
|
|
3753
3883
|
$("#module-header-actions").insertAdjacentHTML("beforeend", `<div id="timeline-tools" class="timeline-tools" data-module-header-action="timeline-tools" role="group" aria-label="时间轴操作"><button id="create-timeline-track" class="ghost-button" type="button">新建独立时间轴</button><button id="timeline-multi-select-toggle" class="ghost-button" type="button" aria-pressed="false">多选</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button" hidden>合并所选事件</button>' : ""}</div>`);
|
|
@@ -3755,9 +3885,10 @@ async function renderTimeline() {
|
|
|
3755
3885
|
const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
|
|
3756
3886
|
const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(timelineStatusLabel(item.status))}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
|
|
3757
3887
|
$("#module-content").innerHTML = `<div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
|
|
3758
|
-
const laneEvents =
|
|
3888
|
+
const laneEvents = pageResult.items.filter((item) => (item.trackId ?? "") === track.id);
|
|
3759
3889
|
return `<section class="timeline-lane" data-track-id="${esc(track.id)}"><header><div><small>${laneEvents.length} 个节点</small><h3>${esc(track.name)}</h3></div>${track.id ? `<div class="timeline-track-actions"><button class="timeline-track-menu" data-edit-timeline-track="${esc(track.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(track.id)}" data-entity-title="${esc(track.name)}" type="button">历史</button></div>` : ""}</header><p class="timeline-track-description">${esc(track.description || "暂无说明")}</p><div class="timeline-lane-events">${laneEvents.map(eventCard).join("") || '<div class="timeline-lane-empty">还没有时间节点</div>'}</div><button class="timeline-add-event" data-add-event-track="${esc(track.id)}" type="button">添加事件</button></section>`;
|
|
3760
|
-
}).join("")}</div
|
|
3890
|
+
}).join("")}</div>${renderModulePagination(pageResult, "timeline", "时间线事件")}`;
|
|
3891
|
+
bindModulePagination("timeline", renderTimeline);
|
|
3761
3892
|
$("#create-timeline-track").addEventListener("click", () => openTimelineTrackDialog());
|
|
3762
3893
|
$("#timeline-multi-select-toggle").addEventListener("click", () => setTimelineMultiSelectMode(!timelineMultiSelectEnabled));
|
|
3763
3894
|
$("#module-content").querySelectorAll("[data-event-select]").forEach((input) => input.addEventListener("change", updateTimelineMultiSelectControls));
|
|
@@ -3766,7 +3897,7 @@ async function renderTimeline() {
|
|
|
3766
3897
|
$("#module-content").querySelectorAll("[data-add-event-track]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(null, button.dataset.addEventTrack || null)));
|
|
3767
3898
|
$("#module-content").querySelectorAll("[data-edit-event]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(events.find((item) => item.id === button.dataset.editEvent))));
|
|
3768
3899
|
$("#module-content").querySelectorAll("[data-split-event]").forEach((button) => button.addEventListener("click", () => openTimelineSplitDialog(events.find((item) => item.id === button.dataset.splitEvent))));
|
|
3769
|
-
bindEntityHistoryButtons(renderTimeline);
|
|
3900
|
+
bindEntityHistoryButtons(() => renderTimeline(pageResult.page));
|
|
3770
3901
|
$("#merge-events")?.addEventListener("click", () => {
|
|
3771
3902
|
const eventIds = [...$("#module-content").querySelectorAll("[data-event-select]:checked")].map((input) => input.dataset.eventSelect);
|
|
3772
3903
|
if (eventIds.length < 2) return toast("请至少选择两个时间事件", "error");
|
|
@@ -3777,30 +3908,34 @@ async function renderTimeline() {
|
|
|
3777
3908
|
description: form.get("description") || undefined,
|
|
3778
3909
|
expectedVersionNos: Object.fromEntries(eventIds.map((eventId) => [eventId, Number(events.find((event) => event.id === eventId)?.versionNo)]))
|
|
3779
3910
|
} });
|
|
3780
|
-
await renderTimeline();
|
|
3911
|
+
await renderTimeline(pageResult.page);
|
|
3781
3912
|
}, "保留参与者与证据");
|
|
3782
3913
|
});
|
|
3783
3914
|
}
|
|
3784
3915
|
|
|
3785
|
-
async function renderOutlines() {
|
|
3916
|
+
async function renderOutlines(outlinePage = moduleListPages.outlinePlans, foreshadowPage = moduleListPages.foreshadows) {
|
|
3786
3917
|
const currentChapterId = state.chapter?.id;
|
|
3787
3918
|
const [outlines, foreshadows] = await Promise.all([
|
|
3788
3919
|
apiAllPages(`/api/works/${state.work.id}/outlines`),
|
|
3789
3920
|
apiAllPages(`/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `¤tChapterId=${encodeURIComponent(currentChapterId)}` : ""}`)
|
|
3790
3921
|
]);
|
|
3791
3922
|
mountModuleCount(outlines.length + foreshadows.length);
|
|
3923
|
+
const outlinePageResult = paginateModuleItems(outlines, outlinePage, "outlines");
|
|
3924
|
+
const foreshadowPageResult = paginateModuleItems(foreshadows, foreshadowPage, "outlines");
|
|
3925
|
+
moduleListPages.outlinePlans = outlinePageResult.page;
|
|
3926
|
+
moduleListPages.foreshadows = foreshadowPageResult.page;
|
|
3792
3927
|
const layout = readModuleLayout();
|
|
3793
3928
|
const unresolved = foreshadows.filter((item) => item.unresolved);
|
|
3794
3929
|
const overdue = unresolved.filter((item) => item.overdue);
|
|
3795
3930
|
const foreshadowActions = (item) => `<button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button>`;
|
|
3796
|
-
const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${
|
|
3931
|
+
const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${foreshadowPageResult.items.map((item) => `
|
|
3797
3932
|
<article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
|
|
3798
3933
|
<small>${esc(levelLabel(item.importance))} · ${esc(foreshadowStatusLabel(item.status))}${item.overdue ? " · 已逾期" : ""}</small>
|
|
3799
3934
|
<h3>${esc(item.title)}</h3><p>${esc(item.description || "暂无说明")}</p>
|
|
3800
3935
|
<div class="foreshadow-links">${item.occurrences.length ? item.occurrences.map((link) => `<span class="pill">${esc(occurrenceRoleLabel(link.role))} · ${esc(link.volumeTitle)} / ${esc(link.chapterTitle)}</span>`).join("") : '<span class="pill">尚未关联章节</span>'}</div>
|
|
3801
3936
|
<div class="card-actions">${foreshadowActions(item)}</div>
|
|
3802
3937
|
</article>`).join("")}</div>`;
|
|
3803
|
-
const foreshadowRows = () => `<div class="module-row-list">${
|
|
3938
|
+
const foreshadowRows = () => `<div class="module-row-list">${foreshadowPageResult.items.map((item) => {
|
|
3804
3939
|
const preview = moduleRowPreview(item.description || "暂无说明");
|
|
3805
3940
|
const links = item.occurrences.length
|
|
3806
3941
|
? item.occurrences.map((link) => `${occurrenceRoleLabel(link.role)} · ${link.volumeTitle} / ${link.chapterTitle}`).join(";")
|
|
@@ -3814,28 +3949,32 @@ async function renderOutlines() {
|
|
|
3814
3949
|
</article>`;
|
|
3815
3950
|
}).join("")}</div>`;
|
|
3816
3951
|
const foreshadowHtml = foreshadows.length
|
|
3817
|
-
? `${layout === "rows" ? foreshadowRows() : foreshadowCards()}`
|
|
3952
|
+
? `${layout === "rows" ? foreshadowRows() : foreshadowCards()}${renderModulePagination(foreshadowPageResult, "foreshadows", "伏笔列表")}`
|
|
3818
3953
|
: emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
|
|
3819
3954
|
if (foreshadows.length) mountModuleLayoutToggle(layout, "伏笔列表样式");
|
|
3820
|
-
const outlineHtml = outlines.length ? `<div class="outline-list">${
|
|
3955
|
+
const outlineHtml = outlines.length ? `<div class="outline-list">${outlinePageResult.items.map((item) => `
|
|
3821
3956
|
<article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
|
|
3822
3957
|
<div><small>${esc(item.volumeTitle)} · ${esc(outlineStatusLabel(item.status))}</small><h3>${esc(item.chapterTitle)}</h3></div>
|
|
3823
3958
|
<div><b>目标</b><p>${esc(item.goal || "未填写")}</p></div>
|
|
3824
3959
|
<div><b>冲突</b><p>${esc(item.conflict || "未填写")}</p></div>
|
|
3825
3960
|
<div><b>转折</b><p>${esc(item.turningPoint || "未填写")}</p></div>
|
|
3826
3961
|
<div class="outline-actions">${item.unresolvedForeshadowCount ? `<span>${item.unresolvedForeshadowCount} 个未回收伏笔</span>` : ""}<button data-edit-outline="${esc(item.chapterId)}">编辑</button><button data-entity-history="chapter-outline" data-entity-id="${esc(item.chapterId)}" data-entity-title="${esc(item.chapterTitle)}">版本历史</button></div>
|
|
3827
|
-
</article>`).join("")}</div
|
|
3962
|
+
</article>`).join("")}</div>${renderModulePagination(outlinePageResult, "outlinePlans", "章节规划")}` : emptyModule("还没有章节", "先创建章节,再为每章维护目标、冲突和转折。\n");
|
|
3828
3963
|
$("#module-content").innerHTML = `<div class="outline-summary"><article><strong>${outlines.length}</strong><span>章节规划</span></article><article><strong>${unresolved.length}</strong><span>未回收伏笔</span></article><article class="${overdue.length ? "danger-text" : ""}"><strong>${overdue.length}</strong><span>已逾期</span></article></div><section class="planning-section"><div class="section-title"><div><span class="eyebrow">伏笔追踪</span><h2>尚未回收与历史伏笔</h2></div></div>${foreshadowHtml}</section><section class="planning-section"><div class="section-title"><div><span class="eyebrow">逐章规划</span><h2>章节目标、冲突与转折</h2></div></div>${outlineHtml}</section>`;
|
|
3829
|
-
bindModuleLayoutToggle(renderOutlines);
|
|
3964
|
+
bindModuleLayoutToggle(() => renderOutlines(outlinePageResult.page, foreshadowPageResult.page));
|
|
3965
|
+
bindModulePagination("foreshadows", (page) => renderOutlines(outlinePageResult.page, page));
|
|
3966
|
+
bindModulePagination("outlinePlans", (page) => renderOutlines(page, foreshadowPageResult.page));
|
|
3830
3967
|
$("#module-content").querySelectorAll("[data-edit-outline]").forEach((button) => button.addEventListener("click", () => openOutlineDialog(outlines.find((item) => item.chapterId === button.dataset.editOutline))));
|
|
3831
3968
|
$("#module-content").querySelectorAll("[data-edit-foreshadow]").forEach((button) => button.addEventListener("click", () => openForeshadowDialog(foreshadows.find((item) => item.id === button.dataset.editForeshadow))));
|
|
3832
|
-
bindEntityHistoryButtons(renderOutlines);
|
|
3969
|
+
bindEntityHistoryButtons(() => renderOutlines(outlinePageResult.page, foreshadowPageResult.page));
|
|
3833
3970
|
}
|
|
3834
3971
|
|
|
3835
|
-
async function renderRelationships() {
|
|
3972
|
+
async function renderRelationships(page = moduleListPages.relationships) {
|
|
3836
3973
|
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
3837
3974
|
const relationships = await apiAllPages(`/api/works/${state.work.id}/relationships`);
|
|
3838
3975
|
const filteredRelationships = filterRelationships(relationships, relationshipFilters);
|
|
3976
|
+
const pageResult = paginateModuleItems(filteredRelationships, page, "relationships");
|
|
3977
|
+
moduleListPages.relationships = pageResult.page;
|
|
3839
3978
|
const hasRelationshipFilters = relationshipFilters.fromCharacterIds.length > 0 || relationshipFilters.toCharacterIds.length > 0;
|
|
3840
3979
|
const canEditRelationships = canEditModule("relationships");
|
|
3841
3980
|
mountModuleCount(filteredRelationships.length);
|
|
@@ -3859,10 +3998,11 @@ async function renderRelationships() {
|
|
|
3859
3998
|
if ($("#relationship-map-dialog").open) $("#relationship-map-dialog").close();
|
|
3860
3999
|
const graph = buildRelationshipGraph(state.characters, relationships);
|
|
3861
4000
|
state.relationshipGraph = graph;
|
|
3862
|
-
const relationshipList = filteredRelationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${
|
|
4001
|
+
const relationshipList = filteredRelationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${pageResult.items.map((item) => `
|
|
3863
4002
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
3864
4003
|
<td>${esc(relationshipCategoryLabel(item.category))} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(relationshipConfirmationLabel(item.confirmationStatus))}</td><td class="relationship-actions">${canEditRelationships ? `<button data-edit-relationship="${esc(item.id)}">编辑</button>` : ""}<button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : relationships.length ? '<div class="relationship-empty-note">没有符合当前筛选条件的关系。</div>' : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>';
|
|
3865
|
-
$("#module-content").innerHTML = `${filterToolbar}<div id="relationship-map-host"></div>${relationshipList}`;
|
|
4004
|
+
$("#module-content").innerHTML = `${filterToolbar}<div id="relationship-map-host"></div>${relationshipList}${renderModulePagination(pageResult, "relationships", "人物关系列表")}`;
|
|
4005
|
+
bindModulePagination("relationships", renderRelationships);
|
|
3866
4006
|
const openGalaxy = () => {
|
|
3867
4007
|
state.galaxy?.destroy();
|
|
3868
4008
|
state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
|
|
@@ -3882,24 +4022,24 @@ async function renderRelationships() {
|
|
|
3882
4022
|
$("#relationship-from-character-filter").addEventListener("change", async () => {
|
|
3883
4023
|
relationshipFiltersPanelOpen = true;
|
|
3884
4024
|
relationshipFilters.fromCharacterIds = readSelectedValues("#relationship-from-character-filter");
|
|
3885
|
-
await renderRelationships();
|
|
4025
|
+
await renderRelationships(1);
|
|
3886
4026
|
});
|
|
3887
4027
|
$("#relationship-to-character-filter").addEventListener("change", async () => {
|
|
3888
4028
|
relationshipFiltersPanelOpen = true;
|
|
3889
4029
|
relationshipFilters.toCharacterIds = readSelectedValues("#relationship-to-character-filter");
|
|
3890
|
-
await renderRelationships();
|
|
4030
|
+
await renderRelationships(1);
|
|
3891
4031
|
});
|
|
3892
4032
|
$("#clear-relationship-filters")?.addEventListener("click", async () => {
|
|
3893
4033
|
relationshipFiltersPanelOpen = true;
|
|
3894
4034
|
relationshipFilters.fromCharacterIds = [];
|
|
3895
4035
|
relationshipFilters.toCharacterIds = [];
|
|
3896
|
-
await renderRelationships();
|
|
4036
|
+
await renderRelationships(1);
|
|
3897
4037
|
});
|
|
3898
4038
|
$("#module-content").querySelectorAll("[data-edit-relationship]").forEach((button) => button.addEventListener("click", () => openRelationshipDialog(filteredRelationships.find((item) => item.id === button.dataset.editRelationship))));
|
|
3899
|
-
bindEntityHistoryButtons(async () => { await renderRelationships(); await loadAiReferences(); });
|
|
4039
|
+
bindEntityHistoryButtons(async () => { await renderRelationships(pageResult.page); await loadAiReferences(); });
|
|
3900
4040
|
}
|
|
3901
4041
|
|
|
3902
|
-
async function renderReviews() {
|
|
4042
|
+
async function renderReviews(page = moduleListPages.reviews) {
|
|
3903
4043
|
const canReadCharacters = canReadModule("characters");
|
|
3904
4044
|
const canResolveReview = canEditModule("reviews");
|
|
3905
4045
|
const canMergeCharacters = canResolveReview
|
|
@@ -3909,6 +4049,8 @@ async function renderReviews() {
|
|
|
3909
4049
|
canReadCharacters ? apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`) : Promise.resolve([])
|
|
3910
4050
|
]);
|
|
3911
4051
|
mountModuleCount(reviews.length);
|
|
4052
|
+
const pageResult = paginateModuleItems(reviews, page, "reviews");
|
|
4053
|
+
moduleListPages.reviews = pageResult.page;
|
|
3912
4054
|
const characterById = new Map(characters.map((character) => [character.id, character]));
|
|
3913
4055
|
const duplicateCard = (item) => {
|
|
3914
4056
|
const refs = (item.entityRefs ?? []).filter((reference) => reference?.type === "character" && characterById.has(reference.id));
|
|
@@ -3946,13 +4088,14 @@ async function renderReviews() {
|
|
|
3946
4088
|
};
|
|
3947
4089
|
if (reviews.length) mountModuleLayoutToggle(layout, "审核列表样式");
|
|
3948
4090
|
$("#module-content").innerHTML = reviews.length
|
|
3949
|
-
? `${layout === "rows" ? `<div class="module-row-list">${
|
|
4091
|
+
? `${layout === "rows" ? `<div class="module-row-list">${pageResult.items.map(reviewRow).join("")}</div>` : `<div class="card-grid">${pageResult.items.map(reviewCard).join("")}</div>`}${renderModulePagination(pageResult, "reviews", "审核列表")}`
|
|
3950
4092
|
: emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
|
|
3951
|
-
bindModuleLayoutToggle(renderReviews);
|
|
4093
|
+
bindModuleLayoutToggle(() => renderReviews(pageResult.page));
|
|
4094
|
+
bindModulePagination("reviews", renderReviews);
|
|
3952
4095
|
bindRecordPreview("[data-open-review]", (id) => openReviewDetailDialog(reviews.find((item) => item.id === id)));
|
|
3953
4096
|
$("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
|
|
3954
4097
|
await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
|
|
3955
|
-
await renderReviews();
|
|
4098
|
+
await renderReviews(pageResult.page);
|
|
3956
4099
|
}));
|
|
3957
4100
|
$("#module-content").querySelectorAll("[data-merge-review]").forEach((button) => button.addEventListener("click", async () => {
|
|
3958
4101
|
const target = characterById.get(button.dataset.mergeTarget);
|
|
@@ -3971,7 +4114,7 @@ async function renderReviews() {
|
|
|
3971
4114
|
expectedSourceVersionNo: Number(button.dataset.sourceVersion)
|
|
3972
4115
|
} });
|
|
3973
4116
|
toast(`已将“${source.name}”合并到“${target.name}”`);
|
|
3974
|
-
await renderReviews();
|
|
4117
|
+
await renderReviews(pageResult.page);
|
|
3975
4118
|
await loadAiReferences();
|
|
3976
4119
|
} catch (error) {
|
|
3977
4120
|
toast(error.message, "error");
|
|
@@ -3983,7 +4126,7 @@ async function renderReviews() {
|
|
|
3983
4126
|
try {
|
|
3984
4127
|
await api(`/api/reviews/${button.dataset.keepCharactersSeparate}/character-resolution`, { method: "POST", body: { action: "keep-separate" } });
|
|
3985
4128
|
toast("已确认这两个档案属于不同角色");
|
|
3986
|
-
await renderReviews();
|
|
4129
|
+
await renderReviews(pageResult.page);
|
|
3987
4130
|
} catch (error) {
|
|
3988
4131
|
toast(error.message, "error");
|
|
3989
4132
|
button.disabled = false;
|
|
@@ -4097,6 +4240,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4097
4240
|
try {
|
|
4098
4241
|
const result = await api(`/api/works/${state.work.id}/tasks/auto-run`, { method: "POST", body: {} });
|
|
4099
4242
|
toast(`已开始下一轮,队列中还有 ${result.pendingCount} 个待执行任务`);
|
|
4243
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
4100
4244
|
await renderTasks();
|
|
4101
4245
|
} catch (error) {
|
|
4102
4246
|
toast(error.message, "error");
|
|
@@ -4135,6 +4279,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4135
4279
|
scheduleTaskProgressRefresh(workId, 1);
|
|
4136
4280
|
const completed = await api(`/api/tasks/${button.dataset.runTask}/run`, { method: "POST", body: {} });
|
|
4137
4281
|
toast(completed.status === "cancelled" ? "分析任务已取消" : completed.status === "expired" ? "正文已变化,本次分析已过期" : "分析已完成");
|
|
4282
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
4138
4283
|
if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
|
|
4139
4284
|
} catch (error) {
|
|
4140
4285
|
toast(error.message, "error");
|
|
@@ -4146,6 +4291,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4146
4291
|
try {
|
|
4147
4292
|
await api(`/api/tasks/${button.dataset.cancelTask}/cancel`, { method: "POST", body: {} });
|
|
4148
4293
|
toast("分析任务已取消");
|
|
4294
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
4149
4295
|
if (state.module === "tasks") await renderTasks();
|
|
4150
4296
|
} catch (error) {
|
|
4151
4297
|
toast(error.message, "error");
|
|
@@ -4155,6 +4301,23 @@ async function renderTasks(page = taskListPage) {
|
|
|
4155
4301
|
scheduleTaskProgressRefresh(state.work.id, runningCount);
|
|
4156
4302
|
}
|
|
4157
4303
|
|
|
4304
|
+
async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
|
|
4305
|
+
const workId = state.work?.id;
|
|
4306
|
+
button.disabled = true;
|
|
4307
|
+
const originalLabel = button.textContent;
|
|
4308
|
+
button.textContent = "正在创建";
|
|
4309
|
+
try {
|
|
4310
|
+
const rerun = await api(`/api/tasks/${encodeURIComponent(taskId)}/rerun`, { method: "POST", body: {} });
|
|
4311
|
+
if (closeDetail) $("#form-dialog").close();
|
|
4312
|
+
toast(`已按原配置创建新任务 ${rerun.id}`);
|
|
4313
|
+
if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
toast(error.message, "error");
|
|
4316
|
+
button.disabled = false;
|
|
4317
|
+
button.textContent = originalLabel;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4158
4321
|
function stopTaskProgressRefresh() {
|
|
4159
4322
|
if (taskProgressRefreshTimer === null) return;
|
|
4160
4323
|
window.clearTimeout(taskProgressRefreshTimer);
|
|
@@ -4373,6 +4536,30 @@ function renderTaskResultItem(item) {
|
|
|
4373
4536
|
</article>`;
|
|
4374
4537
|
}
|
|
4375
4538
|
|
|
4539
|
+
function renderRelationshipChangePreview(task, result) {
|
|
4540
|
+
const preview = result.relationshipChangePreview && typeof result.relationshipChangePreview === "object"
|
|
4541
|
+
? result.relationshipChangePreview
|
|
4542
|
+
: null;
|
|
4543
|
+
if (!preview) return "";
|
|
4544
|
+
const totalCount = Number(preview.totalCount ?? 0);
|
|
4545
|
+
const counts = `新增 ${Number(preview.createdCount ?? 0)} 项 · 更新 ${Number(preview.updatedCount ?? 0)} 项 · 删除 ${Number(preview.deletedCount ?? 0)} 项`;
|
|
4546
|
+
if (preview.status === "pending") {
|
|
4547
|
+
return `<section class="relationship-change-preview is-pending" aria-label="人物关系变更待确认">
|
|
4548
|
+
<div><small>写入前预览</small><h4>人物关系库尚未修改</h4><p>${esc(counts)}。请先核对下方关系和原文证据,再决定是否应用。</p></div>
|
|
4549
|
+
${Number(preview.deletedCount ?? 0) > 0 ? `<p class="relationship-change-preview-warning">确认应用后将删除 ${Number(preview.deletedCount)} 条已有关系;若资料已被他人修改,系统会阻止过期预览覆盖新版本。</p>` : ""}
|
|
4550
|
+
<div class="relationship-change-preview-actions">
|
|
4551
|
+
${totalCount > 0 && canEditModule("relationships") ? `<button class="primary-button" type="button" data-apply-relationship-changes="${esc(task.id)}">确认并应用 ${totalCount} 项变更</button>` : ""}
|
|
4552
|
+
<button class="ghost-button" type="button" data-discard-relationship-changes="${esc(task.id)}">${totalCount > 0 ? "放弃本次变更" : "结束无变更预览"}</button>
|
|
4553
|
+
${!canEditModule("relationships") && totalCount > 0 ? "<small>当前账号没有编辑人物关系的权限,只能查看或放弃本次变更。</small>" : ""}
|
|
4554
|
+
</div>
|
|
4555
|
+
</section>`;
|
|
4556
|
+
}
|
|
4557
|
+
const applied = preview.status === "applied";
|
|
4558
|
+
return `<section class="relationship-change-preview ${applied ? "is-applied" : "is-discarded"}" aria-label="人物关系变更状态">
|
|
4559
|
+
<div><small>写入前预览</small><h4>${applied ? "变更已应用" : "本次变更已放弃"}</h4><p>${esc(counts)}。${applied ? "人物关系库已按确认结果更新。" : "人物关系库未因本次预览发生修改。"}</p></div>
|
|
4560
|
+
</section>`;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4376
4563
|
function renderTaskResult(task) {
|
|
4377
4564
|
const result = task.resultSummary && typeof task.resultSummary === "object" ? task.resultSummary : {};
|
|
4378
4565
|
const metrics = Array.isArray(result.metrics) ? result.metrics : [];
|
|
@@ -4385,6 +4572,7 @@ function renderTaskResult(task) {
|
|
|
4385
4572
|
<p class="task-result-summary">${esc(result.summary || "任务尚未产生分析结果。")}</p>
|
|
4386
4573
|
${result.restricted ? '<p class="task-result-warning">部分结果因当前账号权限受限而隐藏。</p>' : ""}
|
|
4387
4574
|
</section>
|
|
4575
|
+
${renderRelationshipChangePreview(task, result)}
|
|
4388
4576
|
<section class="task-result-section">
|
|
4389
4577
|
<h4>结果保存位置</h4>
|
|
4390
4578
|
<p><strong>作品</strong> ${esc(state.work?.title || "当前作品")}</p>
|
|
@@ -4426,6 +4614,111 @@ function bindTaskResultActions(container) {
|
|
|
4426
4614
|
button.textContent = "重新加载完整 JSON";
|
|
4427
4615
|
}
|
|
4428
4616
|
}));
|
|
4617
|
+
container.querySelectorAll("[data-apply-relationship-changes], [data-discard-relationship-changes]").forEach((button) => button.addEventListener("click", async () => {
|
|
4618
|
+
if (button.disabled) return;
|
|
4619
|
+
const taskId = button.dataset.applyRelationshipChanges || button.dataset.discardRelationshipChanges;
|
|
4620
|
+
const action = button.dataset.applyRelationshipChanges ? "apply" : "discard";
|
|
4621
|
+
const controls = button.closest(".relationship-change-preview")?.querySelectorAll("button") ?? [];
|
|
4622
|
+
controls.forEach((control) => { control.disabled = true; });
|
|
4623
|
+
button.textContent = action === "apply" ? "正在应用变更" : "正在放弃变更";
|
|
4624
|
+
try {
|
|
4625
|
+
await api(`/api/tasks/${encodeURIComponent(taskId)}/relationship-changes/${action}`, { method: "POST", body: {} });
|
|
4626
|
+
$("#form-dialog").close();
|
|
4627
|
+
toast(action === "apply" ? "人物关系变更已应用" : "本次人物关系变更已放弃");
|
|
4628
|
+
if (state.module === "tasks") await renderTasks();
|
|
4629
|
+
} catch (error) {
|
|
4630
|
+
toast(error.message, "error");
|
|
4631
|
+
controls.forEach((control) => { control.disabled = false; });
|
|
4632
|
+
button.textContent = action === "apply" ? "重新确认并应用" : "重新放弃本次变更";
|
|
4633
|
+
}
|
|
4634
|
+
}));
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
function relationshipIdentityRepairFailure(task) {
|
|
4638
|
+
if (task?.taskType !== "relationship-analysis" || !Array.isArray(task.failures)) return null;
|
|
4639
|
+
return task.failures.find((failure) =>
|
|
4640
|
+
failure?.code === "RELATIONSHIP_MATCH_CANDIDATES_EXCEEDED"
|
|
4641
|
+
&& failure.details
|
|
4642
|
+
&& typeof failure.details === "object"
|
|
4643
|
+
&& typeof failure.details.characterId === "string"
|
|
4644
|
+
) ?? null;
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
function relationshipIdentityRepairMetrics(details) {
|
|
4648
|
+
const metrics = [];
|
|
4649
|
+
if (Number.isFinite(Number(details.candidateCount))) {
|
|
4650
|
+
metrics.push(["疑似来源", `${Number(details.candidateCount)} / 上限 ${Number(details.maximum)}`]);
|
|
4651
|
+
}
|
|
4652
|
+
if (Number.isFinite(Number(details.fuzzyReferenceCount))) {
|
|
4653
|
+
metrics.push(["名称与别名", `${Number(details.fuzzyReferenceCount)} / 上限 ${Number(details.maximumFuzzyReferences)}`]);
|
|
4654
|
+
}
|
|
4655
|
+
if (Number.isFinite(Number(details.scannedCharacters))) {
|
|
4656
|
+
metrics.push(["待核对文本", `${Number(details.scannedCharacters).toLocaleString("zh-CN")} / 上限 ${Number(details.maximumScannedCharacters).toLocaleString("zh-CN")} 字符`]);
|
|
4657
|
+
}
|
|
4658
|
+
if (Number.isFinite(Number(details.fuzzyMatchCount))) {
|
|
4659
|
+
metrics.push(["疑似写法", `${Number(details.fuzzyMatchCount)} / 上限 ${Number(details.maximumFuzzyMatches)}`]);
|
|
4660
|
+
}
|
|
4661
|
+
metrics.push(["身份锚点", `${Number(details.identityAnchorCount ?? 0)} 个`]);
|
|
4662
|
+
return metrics;
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
async function openRelationshipIdentityRepairDialog(task, failure) {
|
|
4666
|
+
const details = failure.details && typeof failure.details === "object" ? failure.details : {};
|
|
4667
|
+
const character = await api(`/api/characters/${encodeURIComponent(details.characterId)}`);
|
|
4668
|
+
const anchors = [
|
|
4669
|
+
character.code,
|
|
4670
|
+
character.attributes?.identity,
|
|
4671
|
+
character.race?.name || character.species,
|
|
4672
|
+
...(Array.isArray(character.organizations) ? character.organizations.map((organization) => organization.name) : [])
|
|
4673
|
+
].map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
4674
|
+
const metrics = relationshipIdentityRepairMetrics(details);
|
|
4675
|
+
const metricHtml = metrics.map(([label, value]) => `<div><dt>${esc(label)}</dt><dd>${esc(value)}</dd></div>`).join("");
|
|
4676
|
+
const anchorHtml = anchors.length
|
|
4677
|
+
? anchors.map((anchor) => `<span>${esc(anchor)}</span>`).join("")
|
|
4678
|
+
: "<em>尚未登记可用于消歧的身份锚点</em>";
|
|
4679
|
+
openDialog("修复人物身份资料",
|
|
4680
|
+
`<section class="relationship-identity-repair-summary">
|
|
4681
|
+
<div><span>匹配诊断</span><h3>${esc(character.name)}</h3><p>${esc(failure.message)}</p></div>
|
|
4682
|
+
<dl>${metricHtml}</dl>
|
|
4683
|
+
<div class="relationship-identity-anchor-list"><strong>当前身份锚点</strong><div>${anchorHtml}</div></div>
|
|
4684
|
+
</section>
|
|
4685
|
+
<p class="relationship-identity-repair-guidance">把原文中确实指向该人物的稳定称呼登记为别名;编号和身份定位应填写会在正文或设定中共同出现的辨识词。保存后,别名命中会转为精确匹配,身份锚点可收窄过多的拼音疑似来源。</p>
|
|
4686
|
+
<label>角色标准名<input type="text" value="${esc(character.name)}" readonly></label>
|
|
4687
|
+
${field("aliases", "确认别名", "item-list", character.aliases ?? [])}
|
|
4688
|
+
${field("code", "人物编号或代号", "text", character.code)}
|
|
4689
|
+
${field("identity", "身份与定位", "text", character.attributes?.identity)}
|
|
4690
|
+
<button class="ghost-button relationship-identity-full-profile" type="button" data-open-identity-character-profile>打开完整人物档案</button>`,
|
|
4691
|
+
async (form) => {
|
|
4692
|
+
const aliases = form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean);
|
|
4693
|
+
const code = String(form.get("code") ?? "").trim();
|
|
4694
|
+
const identity = String(form.get("identity") ?? "").trim();
|
|
4695
|
+
const saved = await api(`/api/characters/${encodeURIComponent(character.id)}`, {
|
|
4696
|
+
method: "PATCH",
|
|
4697
|
+
body: {
|
|
4698
|
+
aliases,
|
|
4699
|
+
code,
|
|
4700
|
+
attributes: { ...(character.attributes ?? {}), identity },
|
|
4701
|
+
expectedVersionNo: character.versionNo,
|
|
4702
|
+
changeNote: `修复人物关系来源匹配:${failure.message}`.slice(0, 500)
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
await loadAiReferences();
|
|
4706
|
+
if (state.module === "tasks") await renderTasks(taskListPage);
|
|
4707
|
+
toast(`“${saved.name}”的身份资料已保存为 v${saved.versionNo},可重新运行原分析配置`);
|
|
4708
|
+
},
|
|
4709
|
+
"人物关系匹配修复",
|
|
4710
|
+
{
|
|
4711
|
+
submitLabel: "保存身份修复",
|
|
4712
|
+
pendingLabel: "保存中…",
|
|
4713
|
+
pendingMessage: "正在保存人物身份资料",
|
|
4714
|
+
errorPrefix: "身份修复失败:",
|
|
4715
|
+
wide: true
|
|
4716
|
+
});
|
|
4717
|
+
$("#dialog-fields").querySelector("[data-open-identity-character-profile]")?.addEventListener("click", async () => {
|
|
4718
|
+
$("#form-dialog").close();
|
|
4719
|
+
await openCharacterEditor(character);
|
|
4720
|
+
activateCharacterEditorTab("profile");
|
|
4721
|
+
});
|
|
4429
4722
|
}
|
|
4430
4723
|
|
|
4431
4724
|
function openTaskDetailDialog(task, trace) {
|
|
@@ -4454,6 +4747,15 @@ function openTaskDetailDialog(task, trace) {
|
|
|
4454
4747
|
const failureHtml = failures.length
|
|
4455
4748
|
? `<ul>${failures.map((item) => `<li>${esc(item.message || JSON.stringify(item))}</li>`).join("")}</ul>`
|
|
4456
4749
|
: "<p>无</p>";
|
|
4750
|
+
const identityRepairFailure = relationshipIdentityRepairFailure(task);
|
|
4751
|
+
const identityRepairHtml = identityRepairFailure
|
|
4752
|
+
? `<section class="relationship-identity-repair-entry">
|
|
4753
|
+
<div><strong>可修复的人物身份匹配问题</strong><p>系统已定位到触发来源超限的角色。补充准确别名或身份锚点后,再按原配置重试。</p></div>
|
|
4754
|
+
${canEditModule("characters")
|
|
4755
|
+
? '<button class="primary-button" type="button" data-task-identity-repair>修复人物身份资料</button>'
|
|
4756
|
+
: "<small>当前账户没有人物模块编辑权限。</small>"}
|
|
4757
|
+
</section>`
|
|
4758
|
+
: "";
|
|
4457
4759
|
const resultPreview = renderTaskResult(task);
|
|
4458
4760
|
openDialog("任务详情",
|
|
4459
4761
|
`<div class="task-detail">
|
|
@@ -4464,8 +4766,9 @@ function openTaskDetailDialog(task, trace) {
|
|
|
4464
4766
|
<p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
|
|
4465
4767
|
<p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
|
|
4466
4768
|
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
4467
|
-
<div><strong>失败信息</strong>${failureHtml}</div>
|
|
4769
|
+
<div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
|
|
4468
4770
|
<div><strong>结果摘要</strong>${resultPreview}</div>
|
|
4771
|
+
${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
|
|
4469
4772
|
</section>
|
|
4470
4773
|
${renderTaskTraceVisualization(trace, task.id)}
|
|
4471
4774
|
</div>`,
|
|
@@ -4474,6 +4777,19 @@ function openTaskDetailDialog(task, trace) {
|
|
|
4474
4777
|
{ submitLabel: "关闭", wide: true, trace: true });
|
|
4475
4778
|
bindTaskTraceCallActions($("#dialog-fields"));
|
|
4476
4779
|
bindTaskResultActions($("#dialog-fields"));
|
|
4780
|
+
$("#dialog-fields").querySelector("[data-rerun-task-detail]")?.addEventListener("click", async (event) => {
|
|
4781
|
+
await rerunAnalysisTask(event.currentTarget.dataset.rerunTaskDetail, event.currentTarget, { closeDetail: true });
|
|
4782
|
+
});
|
|
4783
|
+
$("#dialog-fields").querySelector("[data-task-identity-repair]")?.addEventListener("click", async (event) => {
|
|
4784
|
+
const button = event.currentTarget;
|
|
4785
|
+
button.disabled = true;
|
|
4786
|
+
$("#form-dialog").close();
|
|
4787
|
+
try {
|
|
4788
|
+
await openRelationshipIdentityRepairDialog(task, identityRepairFailure);
|
|
4789
|
+
} catch (error) {
|
|
4790
|
+
toast(`身份修复向导加载失败:${error.message}`, "error");
|
|
4791
|
+
}
|
|
4792
|
+
});
|
|
4477
4793
|
}
|
|
4478
4794
|
|
|
4479
4795
|
function renderProviderCards(providers, models) {
|
|
@@ -4518,6 +4834,266 @@ function renderTaskDefaults(models, providers, taskDefaults) {
|
|
|
4518
4834
|
</section>` : emptyModule("尚未配置平台模型", "请先在平台 AI 管理中添加并测试供应商模型。");
|
|
4519
4835
|
}
|
|
4520
4836
|
|
|
4837
|
+
const relationshipIndexStatusLabels = Object.freeze({
|
|
4838
|
+
queued: "等待同步",
|
|
4839
|
+
building: "正在同步",
|
|
4840
|
+
ready: "已索引",
|
|
4841
|
+
failed: "同步失败"
|
|
4842
|
+
});
|
|
4843
|
+
|
|
4844
|
+
const relationshipIndexSourceLabels = Object.freeze({
|
|
4845
|
+
work: "作品资料",
|
|
4846
|
+
chapter: "正文",
|
|
4847
|
+
setting: "设定",
|
|
4848
|
+
character: "人物",
|
|
4849
|
+
race: "种族",
|
|
4850
|
+
organization: "组织",
|
|
4851
|
+
"timeline-track": "时间轴",
|
|
4852
|
+
"timeline-event": "时间线事件",
|
|
4853
|
+
relationship: "人物关系",
|
|
4854
|
+
"chapter-outline": "章节大纲",
|
|
4855
|
+
foreshadow: "伏笔",
|
|
4856
|
+
review: "审核项"
|
|
4857
|
+
});
|
|
4858
|
+
|
|
4859
|
+
function relationshipIndexStatusMarkup(status) {
|
|
4860
|
+
const queuedSources = Array.isArray(status.queuedSources) ? status.queuedSources : [];
|
|
4861
|
+
const statusName = relationshipIndexStatusLabels[status.status] ?? "状态未知";
|
|
4862
|
+
const queueMarkup = queuedSources.length
|
|
4863
|
+
? queuedSources.map((item) => `<span class="relationship-index-queue-chip"><span>${esc(relationshipIndexSourceLabels[item.sourceType] ?? item.sourceType)}</span><strong>${esc(String(item.count))}</strong></span>`).join("")
|
|
4864
|
+
: '<span class="relationship-index-queue-empty">队列为空</span>';
|
|
4865
|
+
const updatedAt = formatDateTime(status.updatedAt) || "尚未构建";
|
|
4866
|
+
const errorMarkup = status.error
|
|
4867
|
+
? `<p class="relationship-index-error">${esc(status.error)}</p>`
|
|
4868
|
+
: "";
|
|
4869
|
+
return `<div class="relationship-index-summary">
|
|
4870
|
+
<div class="relationship-index-state-row"><span class="relationship-index-state is-${esc(String(status.status ?? "unknown"))}">${esc(statusName)}</span><span>索引代次 <strong>${esc(String(status.generation ?? 0))}</strong></span><span>最后更新 <strong>${esc(updatedAt)}</strong></span></div>
|
|
4871
|
+
<dl class="relationship-index-metrics"><div><dt>待同步任务</dt><dd>${esc(String(status.queuedSourceCount ?? 0))}</dd></div><div><dt>已索引正文段落</dt><dd>${esc(String(status.indexedParagraphCount ?? 0))}</dd></div><div><dt>已索引设定来源</dt><dd>${esc(String(status.indexedSourceCount ?? 0))}</dd></div></dl>
|
|
4872
|
+
<div class="relationship-index-queue"><div class="relationship-index-queue-heading"><strong>增量任务队列</strong><span>仅包含新增、修改或删除后待同步的来源</span></div><div class="relationship-index-queue-list">${queueMarkup}</div></div>
|
|
4873
|
+
${errorMarkup}
|
|
4874
|
+
</div>`;
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
function updateBackgroundTaskCenterVisibility() {
|
|
4878
|
+
const button = $("#background-task-button");
|
|
4879
|
+
if (!button) return;
|
|
4880
|
+
const visible = Boolean(state.work)
|
|
4881
|
+
&& (canReadModule("tasks") || canReadModule("ai-settings"));
|
|
4882
|
+
button.classList.toggle("hidden", !visible);
|
|
4883
|
+
if (!visible) {
|
|
4884
|
+
$("#background-task-count")?.classList.add("hidden");
|
|
4885
|
+
return;
|
|
4886
|
+
}
|
|
4887
|
+
const activityCount = backgroundTaskActivityCount(
|
|
4888
|
+
backgroundTaskCenterSnapshot.taskPage,
|
|
4889
|
+
backgroundTaskCenterSnapshot.relationshipIndex
|
|
4890
|
+
);
|
|
4891
|
+
const badge = $("#background-task-count");
|
|
4892
|
+
badge.textContent = activityCount > 99 ? "99+" : String(activityCount);
|
|
4893
|
+
badge.classList.toggle("hidden", activityCount === 0);
|
|
4894
|
+
button.classList.toggle("is-active", activityCount > 0);
|
|
4895
|
+
button.setAttribute("aria-label", activityCount > 0 ? `后台任务中心,${activityCount} 项进行中` : "后台任务中心");
|
|
4896
|
+
button.setAttribute("title", activityCount > 0 ? `后台任务中心 · ${activityCount} 项进行中` : "后台任务中心");
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
function backgroundTaskTransitionMessage(transition) {
|
|
4900
|
+
const label = analysisTaskTypeLabel(transition.task.taskType);
|
|
4901
|
+
if (transition.status === "partial") return { message: `${label}部分失败,请打开任务详情查看`, type: "error" };
|
|
4902
|
+
if (transition.status === "expired") return { message: `${label}已过期,正文可能已发生变化`, type: "error" };
|
|
4903
|
+
if (transition.status === "cancelled") return { message: `${label}已取消`, type: "info" };
|
|
4904
|
+
return { message: `${label}已完成`, type: "info" };
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
function backgroundTaskListMarkup(taskPage, error) {
|
|
4908
|
+
if (!canReadModule("tasks")) {
|
|
4909
|
+
return '<section class="background-task-section"><div class="background-task-section-heading"><div><strong>AI 分析任务</strong><small>当前账户无权查看此模块</small></div></div></section>';
|
|
4910
|
+
}
|
|
4911
|
+
if (!taskPage) {
|
|
4912
|
+
return `<section class="background-task-section"><div class="background-task-section-heading"><div><strong>AI 分析任务</strong><small>${esc(error || "正在读取任务队列")}</small></div></div></section>`;
|
|
4913
|
+
}
|
|
4914
|
+
const tasks = Array.isArray(taskPage.items) ? taskPage.items.slice(0, 10) : [];
|
|
4915
|
+
const pendingCount = Number(taskPage.stats?.pendingCount ?? 0);
|
|
4916
|
+
const runningCount = Number(taskPage.stats?.runningCount ?? 0);
|
|
4917
|
+
return `<section class="background-task-section">
|
|
4918
|
+
<div class="background-task-section-heading"><div><strong>AI 分析任务</strong><small>待执行 ${pendingCount} 个 · 运行中 ${runningCount} 个 · 共 ${Number(taskPage.stats?.total ?? taskPage.total ?? tasks.length)} 个</small></div></div>
|
|
4919
|
+
${error ? `<p class="background-task-error">${esc(error)}</p>` : ""}
|
|
4920
|
+
${tasks.length ? `<div class="background-task-list">${tasks.map((item) => {
|
|
4921
|
+
const status = normalizedAnalysisTaskStatus(item.status);
|
|
4922
|
+
return `<article class="background-task-row">
|
|
4923
|
+
<span class="task-status-badge is-${esc(status)}"><span class="task-status-indicator" aria-hidden="true"></span><span>${esc(analysisTaskStatusLabel(item.status))}</span></span>
|
|
4924
|
+
<div><strong>${esc(analysisTaskTypeLabel(item.taskType))}</strong><small>${esc(item.scopeSummary || "未指定范围")} · ${esc(item.model?.displayName || "默认模型")}</small></div>
|
|
4925
|
+
<span class="background-task-progress">${analysisTaskProgressValue(item.progress)}%</span>
|
|
4926
|
+
<button class="ghost-button" type="button" data-background-task-detail="${esc(item.id)}">详情</button>
|
|
4927
|
+
</article>`;
|
|
4928
|
+
}).join("")}</div>` : '<p class="background-task-empty">还没有 AI 分析任务。</p>'}
|
|
4929
|
+
</section>`;
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
function backgroundIndexMarkup(relationshipIndex, error) {
|
|
4933
|
+
if (!canReadModule("ai-settings")) {
|
|
4934
|
+
return '<section class="background-task-section"><div class="background-task-section-heading"><div><strong>人物关系拼音索引</strong><small>当前账户无权查看此模块</small></div></div></section>';
|
|
4935
|
+
}
|
|
4936
|
+
if (!relationshipIndex) {
|
|
4937
|
+
return `<section class="background-task-section"><div class="background-task-section-heading"><div><strong>人物关系拼音索引</strong><small>${esc(error || "正在读取索引队列")}</small></div></div></section>`;
|
|
4938
|
+
}
|
|
4939
|
+
const editable = canEditModule("ai-settings");
|
|
4940
|
+
return `<section class="background-task-section">
|
|
4941
|
+
<div class="background-task-section-heading">
|
|
4942
|
+
<div><strong>人物关系拼音索引</strong><small>可在任何模块查看增量同步状态</small></div>
|
|
4943
|
+
<div class="background-index-actions ${editable ? "" : "hidden"}">
|
|
4944
|
+
<button class="primary-button" type="button" data-background-index-action="sync">同步增量队列</button>
|
|
4945
|
+
<button class="ghost-button" type="button" data-background-index-action="rebuild">完整重建</button>
|
|
4946
|
+
</div>
|
|
4947
|
+
</div>
|
|
4948
|
+
${error ? `<p class="background-task-error">${esc(error)}</p>` : ""}
|
|
4949
|
+
${relationshipIndexStatusMarkup(relationshipIndex)}
|
|
4950
|
+
</section>`;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
function renderBackgroundTaskCenter() {
|
|
4954
|
+
updateBackgroundTaskCenterVisibility();
|
|
4955
|
+
const content = $("#background-task-content");
|
|
4956
|
+
if (!content) return;
|
|
4957
|
+
content.innerHTML = `${backgroundTaskListMarkup(
|
|
4958
|
+
backgroundTaskCenterSnapshot.taskPage,
|
|
4959
|
+
backgroundTaskCenterSnapshot.errors.tasks
|
|
4960
|
+
)}${backgroundIndexMarkup(
|
|
4961
|
+
backgroundTaskCenterSnapshot.relationshipIndex,
|
|
4962
|
+
backgroundTaskCenterSnapshot.errors.index
|
|
4963
|
+
)}`;
|
|
4964
|
+
$("#background-task-dialog-meta").textContent = state.work
|
|
4965
|
+
? `《${state.work.title}》的分析任务与索引队列`
|
|
4966
|
+
: "当前作品的分析任务与索引队列";
|
|
4967
|
+
$("#background-task-open-analysis").classList.toggle("hidden", !canReadModule("tasks"));
|
|
4968
|
+
content.querySelectorAll("[data-background-task-detail]").forEach((button) => button.addEventListener("click", async () => {
|
|
4969
|
+
button.disabled = true;
|
|
4970
|
+
try {
|
|
4971
|
+
const taskId = encodeURIComponent(button.dataset.backgroundTaskDetail);
|
|
4972
|
+
const [task, trace] = await Promise.all([
|
|
4973
|
+
api(`/api/tasks/${taskId}/detail`),
|
|
4974
|
+
api(`/api/tasks/${taskId}/trace`).catch((error) => {
|
|
4975
|
+
if (error.code === "WORK_MODULE_READ_DENIED") return { restricted: true, captured: false, calls: [] };
|
|
4976
|
+
throw error;
|
|
4977
|
+
})
|
|
4978
|
+
]);
|
|
4979
|
+
$("#background-task-dialog").close();
|
|
4980
|
+
openTaskDetailDialog(task, trace);
|
|
4981
|
+
} catch (error) {
|
|
4982
|
+
toast(error.message, "error");
|
|
4983
|
+
button.disabled = false;
|
|
4984
|
+
}
|
|
4985
|
+
}));
|
|
4986
|
+
content.querySelectorAll("[data-background-index-action]").forEach((button) => button.addEventListener("click", async () => {
|
|
4987
|
+
if (!backgroundTaskCenterWorkId) return;
|
|
4988
|
+
button.disabled = true;
|
|
4989
|
+
const action = button.dataset.backgroundIndexAction;
|
|
4990
|
+
try {
|
|
4991
|
+
const status = await api(`/api/works/${encodeURIComponent(backgroundTaskCenterWorkId)}/ai-settings/relationship-search-index/${action}`, {
|
|
4992
|
+
method: "POST"
|
|
4993
|
+
});
|
|
4994
|
+
backgroundTaskCenterSnapshot.relationshipIndex = status;
|
|
4995
|
+
renderBackgroundTaskCenter();
|
|
4996
|
+
toast(action === "rebuild"
|
|
4997
|
+
? `已将全部来源加入索引队列,共 ${status.queuedSourceCount} 项`
|
|
4998
|
+
: Number(status.queuedSourceCount) > 0
|
|
4999
|
+
? `开始同步 ${status.queuedSourceCount} 项增量任务`
|
|
5000
|
+
: "增量任务队列为空,索引已是最新状态");
|
|
5001
|
+
scheduleBackgroundTaskCenterRefresh();
|
|
5002
|
+
} catch (error) {
|
|
5003
|
+
toast(error.message, "error");
|
|
5004
|
+
button.disabled = false;
|
|
5005
|
+
}
|
|
5006
|
+
}));
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
function scheduleBackgroundTaskCenterRefresh() {
|
|
5010
|
+
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
5011
|
+
backgroundTaskCenterTimer = null;
|
|
5012
|
+
if (!backgroundTaskCenterWorkId) return;
|
|
5013
|
+
const activityCount = backgroundTaskActivityCount(
|
|
5014
|
+
backgroundTaskCenterSnapshot.taskPage,
|
|
5015
|
+
backgroundTaskCenterSnapshot.relationshipIndex
|
|
5016
|
+
);
|
|
5017
|
+
const delay = backgroundTaskPollDelay(activityCount, Boolean($("#background-task-dialog")?.open));
|
|
5018
|
+
backgroundTaskCenterTimer = window.setTimeout(() => {
|
|
5019
|
+
backgroundTaskCenterTimer = null;
|
|
5020
|
+
void refreshBackgroundTaskCenter();
|
|
5021
|
+
}, delay);
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
async function refreshBackgroundTaskCenter({ announce = true } = {}) {
|
|
5025
|
+
const workId = backgroundTaskCenterWorkId;
|
|
5026
|
+
if (!workId || state.work?.id !== workId) return;
|
|
5027
|
+
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
5028
|
+
backgroundTaskCenterTimer = null;
|
|
5029
|
+
const requestId = ++backgroundTaskCenterRequest;
|
|
5030
|
+
const readTasks = canReadModule("tasks");
|
|
5031
|
+
const readIndex = canReadModule("ai-settings");
|
|
5032
|
+
const [taskResult, indexResult] = await Promise.all([
|
|
5033
|
+
readTasks
|
|
5034
|
+
? api(`/api/works/${encodeURIComponent(workId)}/tasks?page=1&limit=30`)
|
|
5035
|
+
.then((value) => ({ value }))
|
|
5036
|
+
.catch((error) => ({ error }))
|
|
5037
|
+
: Promise.resolve(null),
|
|
5038
|
+
readIndex
|
|
5039
|
+
? api(`/api/works/${encodeURIComponent(workId)}/ai-settings/relationship-search-index`)
|
|
5040
|
+
.then((value) => ({ value }))
|
|
5041
|
+
.catch((error) => ({ error }))
|
|
5042
|
+
: Promise.resolve(null)
|
|
5043
|
+
]);
|
|
5044
|
+
if (requestId !== backgroundTaskCenterRequest || backgroundTaskCenterWorkId !== workId || state.work?.id !== workId) return;
|
|
5045
|
+
const errors = {};
|
|
5046
|
+
if (taskResult?.value) {
|
|
5047
|
+
const transitionResult = collectBackgroundTaskTransitions(
|
|
5048
|
+
backgroundTaskCenterTaskSnapshots,
|
|
5049
|
+
taskResult.value.items,
|
|
5050
|
+
backgroundTaskCenterTasksInitialized
|
|
5051
|
+
);
|
|
5052
|
+
backgroundTaskCenterTaskSnapshots = transitionResult.snapshots;
|
|
5053
|
+
backgroundTaskCenterSnapshot.taskPage = taskResult.value;
|
|
5054
|
+
if (backgroundTaskCenterTasksInitialized && announce && state.module !== "tasks") {
|
|
5055
|
+
for (const transition of transitionResult.transitions) {
|
|
5056
|
+
const notification = backgroundTaskTransitionMessage(transition);
|
|
5057
|
+
toast(notification.message, notification.type);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
backgroundTaskCenterTasksInitialized = true;
|
|
5061
|
+
} else if (taskResult?.error) {
|
|
5062
|
+
errors.tasks = taskResult.error.message;
|
|
5063
|
+
console.error("Failed to refresh background analysis tasks", taskResult.error);
|
|
5064
|
+
}
|
|
5065
|
+
if (indexResult?.value) {
|
|
5066
|
+
backgroundTaskCenterSnapshot.relationshipIndex = indexResult.value;
|
|
5067
|
+
} else if (indexResult?.error) {
|
|
5068
|
+
errors.index = indexResult.error.message;
|
|
5069
|
+
console.error("Failed to refresh relationship search index status", indexResult.error);
|
|
5070
|
+
}
|
|
5071
|
+
backgroundTaskCenterSnapshot.errors = errors;
|
|
5072
|
+
renderBackgroundTaskCenter();
|
|
5073
|
+
scheduleBackgroundTaskCenterRefresh();
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
function stopBackgroundTaskCenter() {
|
|
5077
|
+
if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
|
|
5078
|
+
backgroundTaskCenterTimer = null;
|
|
5079
|
+
backgroundTaskCenterRequest += 1;
|
|
5080
|
+
backgroundTaskCenterWorkId = null;
|
|
5081
|
+
backgroundTaskCenterTasksInitialized = false;
|
|
5082
|
+
backgroundTaskCenterTaskSnapshots = new Map();
|
|
5083
|
+
backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
5084
|
+
const dialog = $("#background-task-dialog");
|
|
5085
|
+
if (dialog?.open) dialog.close();
|
|
5086
|
+
$("#background-task-button")?.classList.add("hidden");
|
|
5087
|
+
$("#background-task-count")?.classList.add("hidden");
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
function startBackgroundTaskCenter(workId) {
|
|
5091
|
+
stopBackgroundTaskCenter();
|
|
5092
|
+
backgroundTaskCenterWorkId = String(workId);
|
|
5093
|
+
renderBackgroundTaskCenter();
|
|
5094
|
+
void refreshBackgroundTaskCenter();
|
|
5095
|
+
}
|
|
5096
|
+
|
|
4521
5097
|
async function renderPlatformAiConfig() {
|
|
4522
5098
|
const [providers, models, settings] = await Promise.all([
|
|
4523
5099
|
api("/api/platform/ai/providers"),
|
|
@@ -4541,16 +5117,116 @@ async function renderPlatformAiConfig() {
|
|
|
4541
5117
|
bindPlatformProviderActions(host, providers, models);
|
|
4542
5118
|
}
|
|
4543
5119
|
|
|
5120
|
+
function tokenUsageDateLabel(date) {
|
|
5121
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
5122
|
+
year: "numeric",
|
|
5123
|
+
month: "short",
|
|
5124
|
+
day: "numeric",
|
|
5125
|
+
weekday: "short"
|
|
5126
|
+
}).format(new Date(`${date}T00:00:00`));
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
function tokenUsageCalendarMarkup(daily) {
|
|
5130
|
+
const calendar = buildUsageCalendar(daily);
|
|
5131
|
+
const cells = calendar.cells.map((cell) => {
|
|
5132
|
+
const label = `${tokenUsageDateLabel(cell.date)}:${Number(cell.totalTokens).toLocaleString("zh-CN")} Token`;
|
|
5133
|
+
return `<span class="usage-calendar-cell${cell.future ? " is-future" : ""}" data-level="${cell.level}" role="gridcell" aria-label="${esc(label)}" title="${esc(label)}" ${cell.future ? 'aria-disabled="true"' : 'tabindex="0"'}></span>`;
|
|
5134
|
+
}).join("");
|
|
5135
|
+
const months = calendar.months.map((month) => `<span style="grid-column:${month.week + 1}">${esc(month.label)}</span>`).join("");
|
|
5136
|
+
return `<div class="usage-calendar-scroll" tabindex="0" aria-label="每日 Token 用量日历,可横向滚动">
|
|
5137
|
+
<div class="usage-calendar-frame" style="--usage-week-count:${calendar.weekCount}">
|
|
5138
|
+
<div class="usage-calendar-months" aria-hidden="true">${months}</div>
|
|
5139
|
+
<div class="usage-calendar-body">
|
|
5140
|
+
<div class="usage-calendar-weekdays" aria-hidden="true"><span>一</span><span>三</span><span>五</span></div>
|
|
5141
|
+
<div class="usage-calendar-grid" role="grid" aria-label="过去 53 周每日 Token 用量">${cells}</div>
|
|
5142
|
+
</div>
|
|
5143
|
+
</div>
|
|
5144
|
+
</div>
|
|
5145
|
+
<div class="usage-calendar-legend"><span>少</span>${[0, 1, 2, 3, 4].map((level) => `<i data-level="${level}" aria-hidden="true"></i>`).join("")}<span>多</span></div>`;
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
function scrollUsageCalendarsToLatest(root) {
|
|
5149
|
+
window.requestAnimationFrame(() => {
|
|
5150
|
+
root.querySelectorAll(".usage-calendar-scroll").forEach((calendar) => {
|
|
5151
|
+
calendar.scrollLeft = calendar.scrollWidth;
|
|
5152
|
+
});
|
|
5153
|
+
});
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false } = {}) {
|
|
5157
|
+
const summary = usage?.summary ?? {};
|
|
5158
|
+
const totalTokens = Number(summary.totalTokens) || 0;
|
|
5159
|
+
const exactTotal = totalTokens.toLocaleString("zh-CN");
|
|
5160
|
+
const estimatedRequests = Number(summary.estimatedRequestCount) || 0;
|
|
5161
|
+
const requestCount = Number(summary.requestCount) || 0;
|
|
5162
|
+
const cachedInputTokens = Number(summary.cachedInputTokens) || 0;
|
|
5163
|
+
const cacheEligibleInputTokens = Number(summary.cacheEligibleInputTokens) || 0;
|
|
5164
|
+
const cacheDescription = summary.cacheHitRate === null || summary.cacheHitRate === undefined
|
|
5165
|
+
? "供应商尚未返回可计算的缓存明细"
|
|
5166
|
+
: `${cachedInputTokens.toLocaleString("zh-CN")} / ${cacheEligibleInputTokens.toLocaleString("zh-CN")} 个可统计输入 Token 命中缓存`;
|
|
5167
|
+
const estimateNote = estimatedRequests > 0
|
|
5168
|
+
? `其中 ${estimatedRequests.toLocaleString("zh-CN")} 次调用包含历史或供应商缺失用量时的估算。`
|
|
5169
|
+
: "全部用量均来自供应商返回的 Token 统计。";
|
|
5170
|
+
const works = Array.isArray(usage?.works) ? usage.works : [];
|
|
5171
|
+
const workRows = works.map((work) => `<tr>
|
|
5172
|
+
<th scope="row">${esc(work.workTitle)}</th>
|
|
5173
|
+
<td title="${esc(Number(work.totalTokens || 0).toLocaleString("zh-CN"))} Token">${esc(formatTokenCount(work.totalTokens))}</td>
|
|
5174
|
+
<td>${esc(formatTokenCount(work.inputTokens))}</td>
|
|
5175
|
+
<td>${esc(formatTokenCount(work.outputTokens))}</td>
|
|
5176
|
+
<td>${esc(formatCacheHitRate(work.cacheHitRate))}</td>
|
|
5177
|
+
<td>${Number(work.requestCount || 0).toLocaleString("zh-CN")}</td>
|
|
5178
|
+
</tr>`).join("");
|
|
5179
|
+
return `<section class="usage-overview" aria-labelledby="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">
|
|
5180
|
+
<div class="config-section-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div></div>
|
|
5181
|
+
<div class="usage-stat-grid">
|
|
5182
|
+
<article class="usage-stat is-primary"><span>总消耗</span><strong title="${esc(exactTotal)} Token">${esc(formatTokenCount(totalTokens))}</strong><small>${esc(exactTotal)} Token</small></article>
|
|
5183
|
+
<article class="usage-stat"><span>输入 Token</span><strong>${esc(formatTokenCount(summary.inputTokens))}</strong><small>${Number(summary.inputTokens || 0).toLocaleString("zh-CN")}</small></article>
|
|
5184
|
+
<article class="usage-stat"><span>输出 Token</span><strong>${esc(formatTokenCount(summary.outputTokens))}</strong><small>${Number(summary.outputTokens || 0).toLocaleString("zh-CN")}</small></article>
|
|
5185
|
+
<article class="usage-stat"><span>缓存命中率</span><strong>${esc(formatCacheHitRate(summary.cacheHitRate))}</strong><small>${esc(cacheDescription)}</small></article>
|
|
5186
|
+
</div>
|
|
5187
|
+
<p class="usage-measurement-note">${requestCount.toLocaleString("zh-CN")} 次有用量记录的调用。${esc(estimateNote)}</p>
|
|
5188
|
+
<section class="usage-calendar-section" aria-labelledby="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">
|
|
5189
|
+
<header><div><h3 id="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">每日用量</h3><p>GitHub 风格网格展示过去 53 周;颜色越深,当天消耗越高。</p></div></header>
|
|
5190
|
+
${tokenUsageCalendarMarkup(usage?.daily)}
|
|
5191
|
+
</section>
|
|
5192
|
+
${showWorks ? `<section class="usage-work-section" aria-labelledby="usage-work-title"><header><div><h3 id="usage-work-title">各作品用量</h3><p>按 Token 总消耗从高到低排列,包含尚未使用 AI 的作品。</p></div></header><div class="usage-work-table-scroll"><table class="usage-work-table"><thead><tr><th>作品</th><th>总消耗</th><th>输入</th><th>输出</th><th>缓存命中率</th><th>调用</th></tr></thead><tbody>${workRows || '<tr><td colspan="6">还没有作品用量记录。</td></tr>'}</tbody></table></div></section>` : ""}
|
|
5193
|
+
</section>`;
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
async function renderPlatformTokenUsage() {
|
|
5197
|
+
const host = $("#platform-usage-content");
|
|
5198
|
+
host.innerHTML = '<div class="empty-state">正在汇总 Token 用量……</div>';
|
|
5199
|
+
const timezoneOffset = -new Date().getTimezoneOffset();
|
|
5200
|
+
const usage = await api(`/api/platform/ai/usage?timezoneOffset=${timezoneOffset}`);
|
|
5201
|
+
host.innerHTML = tokenUsageOverviewMarkup(usage, {
|
|
5202
|
+
title: "项目累计用量",
|
|
5203
|
+
description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
|
|
5204
|
+
showWorks: true
|
|
5205
|
+
});
|
|
5206
|
+
scrollUsageCalendarsToLatest(host);
|
|
5207
|
+
}
|
|
5208
|
+
|
|
4544
5209
|
async function renderBookAiSettings() {
|
|
4545
|
-
|
|
5210
|
+
if (relationshipSearchIndexRefreshTimer) {
|
|
5211
|
+
clearTimeout(relationshipSearchIndexRefreshTimer);
|
|
5212
|
+
relationshipSearchIndexRefreshTimer = null;
|
|
5213
|
+
}
|
|
5214
|
+
const [settings, providers, models, taskDefaults, relationshipIndex, usage] = await Promise.all([
|
|
4546
5215
|
api(`/api/works/${state.work.id}/ai-settings`),
|
|
4547
5216
|
api("/api/platform/ai/providers"),
|
|
4548
5217
|
api(`/api/works/${state.work.id}/models`),
|
|
4549
|
-
api(`/api/works/${state.work.id}/task-defaults`)
|
|
5218
|
+
api(`/api/works/${state.work.id}/task-defaults`),
|
|
5219
|
+
api(`/api/works/${state.work.id}/ai-settings/relationship-search-index`),
|
|
5220
|
+
api(`/api/works/${state.work.id}/ai-settings/usage?timezoneOffset=${-new Date().getTimezoneOffset()}`)
|
|
4550
5221
|
]);
|
|
4551
5222
|
const host = $("#module-content");
|
|
5223
|
+
const workId = String(state.work.id);
|
|
4552
5224
|
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"]);
|
|
4553
|
-
host.innerHTML = `<section class="config-section"
|
|
5225
|
+
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
5226
|
+
title: "本书 Token 用量",
|
|
5227
|
+
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
5228
|
+
})}</section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>对话 context 使用独立预算。达到该百分比阈值时先提醒;继续发送会对较早消息执行 compact,压缩上下文占用,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名或关键词子串匹配设定、人物、组织、时间线、关系、大纲和伏笔;非语义检索。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults)}`;
|
|
5229
|
+
scrollUsageCalendarsToLatest(host);
|
|
4554
5230
|
host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
|
|
4555
5231
|
"beforebegin",
|
|
4556
5232
|
`<label><input name="agent-tool" type="checkbox" value="grep" ${agentTools.has("grep") ? "checked" : ""}><span><strong>查询正文关键字</strong><small>从段落索引查询关键字,默认返回前 20 条完整段落和章节信息。</small></span></label>`
|
|
@@ -4563,6 +5239,33 @@ async function renderBookAiSettings() {
|
|
|
4563
5239
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
4564
5240
|
host.querySelectorAll(".config-save-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
4565
5241
|
}
|
|
5242
|
+
const isCurrentRelationshipIndexPanel = () => state.module === "ai-settings"
|
|
5243
|
+
&& String(state.work?.id ?? "") === workId
|
|
5244
|
+
&& Boolean($("#relationship-search-index-status"));
|
|
5245
|
+
const updateRelationshipIndexStatus = (status) => {
|
|
5246
|
+
const statusHost = $("#relationship-search-index-status");
|
|
5247
|
+
if (!statusHost) return;
|
|
5248
|
+
statusHost.innerHTML = relationshipIndexStatusMarkup(status);
|
|
5249
|
+
if (relationshipSearchIndexRefreshTimer) clearTimeout(relationshipSearchIndexRefreshTimer);
|
|
5250
|
+
relationshipSearchIndexRefreshTimer = null;
|
|
5251
|
+
if (!["queued", "building"].includes(String(status.status))) return;
|
|
5252
|
+
relationshipSearchIndexRefreshTimer = setTimeout(async () => {
|
|
5253
|
+
relationshipSearchIndexRefreshTimer = null;
|
|
5254
|
+
if (!isCurrentRelationshipIndexPanel()) return;
|
|
5255
|
+
try {
|
|
5256
|
+
const nextStatus = await api(`/api/works/${workId}/ai-settings/relationship-search-index`);
|
|
5257
|
+
if (isCurrentRelationshipIndexPanel()) updateRelationshipIndexStatus(nextStatus);
|
|
5258
|
+
} catch {
|
|
5259
|
+
// 后台轮询失败时保留当前状态,用户仍可手动刷新。
|
|
5260
|
+
}
|
|
5261
|
+
}, 1_200);
|
|
5262
|
+
};
|
|
5263
|
+
const refreshRelationshipIndexStatus = async () => {
|
|
5264
|
+
const status = await api(`/api/works/${workId}/ai-settings/relationship-search-index`);
|
|
5265
|
+
updateRelationshipIndexStatus(status);
|
|
5266
|
+
return status;
|
|
5267
|
+
};
|
|
5268
|
+
updateRelationshipIndexStatus(relationshipIndex);
|
|
4566
5269
|
$("#save-work-system-prompt").addEventListener("click", async () => {
|
|
4567
5270
|
const button = $("#save-work-system-prompt");
|
|
4568
5271
|
button.disabled = true;
|
|
@@ -4576,6 +5279,46 @@ async function renderBookAiSettings() {
|
|
|
4576
5279
|
button.disabled = false;
|
|
4577
5280
|
}
|
|
4578
5281
|
});
|
|
5282
|
+
$("#sync-relationship-search-index").addEventListener("click", async () => {
|
|
5283
|
+
const button = $("#sync-relationship-search-index");
|
|
5284
|
+
button.disabled = true;
|
|
5285
|
+
try {
|
|
5286
|
+
const status = await api(`/api/works/${workId}/ai-settings/relationship-search-index/sync`, { method: "POST" });
|
|
5287
|
+
updateRelationshipIndexStatus(status);
|
|
5288
|
+
toast(Number(status.queuedSourceCount) > 0
|
|
5289
|
+
? `开始同步 ${status.queuedSourceCount} 项增量任务`
|
|
5290
|
+
: "增量任务队列为空,索引已是最新状态");
|
|
5291
|
+
} catch (error) {
|
|
5292
|
+
toast(error.message, "error");
|
|
5293
|
+
} finally {
|
|
5294
|
+
button.disabled = false;
|
|
5295
|
+
}
|
|
5296
|
+
});
|
|
5297
|
+
$("#refresh-relationship-search-index").addEventListener("click", async () => {
|
|
5298
|
+
const button = $("#refresh-relationship-search-index");
|
|
5299
|
+
button.disabled = true;
|
|
5300
|
+
try {
|
|
5301
|
+
await refreshRelationshipIndexStatus();
|
|
5302
|
+
toast("索引状态已刷新", "info");
|
|
5303
|
+
} catch (error) {
|
|
5304
|
+
toast(error.message, "error");
|
|
5305
|
+
} finally {
|
|
5306
|
+
button.disabled = false;
|
|
5307
|
+
}
|
|
5308
|
+
});
|
|
5309
|
+
$("#rebuild-relationship-search-index").addEventListener("click", async () => {
|
|
5310
|
+
const button = $("#rebuild-relationship-search-index");
|
|
5311
|
+
button.disabled = true;
|
|
5312
|
+
try {
|
|
5313
|
+
const status = await api(`/api/works/${workId}/ai-settings/relationship-search-index/rebuild`, { method: "POST" });
|
|
5314
|
+
updateRelationshipIndexStatus(status);
|
|
5315
|
+
toast(`已将全部来源加入索引队列,共 ${status.queuedSourceCount} 项`);
|
|
5316
|
+
} catch (error) {
|
|
5317
|
+
toast(error.message, "error");
|
|
5318
|
+
} finally {
|
|
5319
|
+
button.disabled = false;
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
4579
5322
|
$("#save-book-summary-context-percent").addEventListener("click", async () => {
|
|
4580
5323
|
const button = $("#save-book-summary-context-percent");
|
|
4581
5324
|
button.disabled = true;
|
|
@@ -6040,9 +6783,10 @@ async function showCharacterHistory() {
|
|
|
6040
6783
|
|
|
6041
6784
|
async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
6042
6785
|
entityEditorReadOnly = readOnly;
|
|
6043
|
-
[state.races, state.organizations] = await Promise.all([
|
|
6786
|
+
[state.races, state.organizations, state.characters] = await Promise.all([
|
|
6044
6787
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
6045
|
-
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
6788
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([]),
|
|
6789
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
6046
6790
|
]);
|
|
6047
6791
|
characterEditorItem = item ?? null;
|
|
6048
6792
|
characterEditorVersions = [];
|
|
@@ -6391,6 +7135,7 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
6391
7135
|
<div><strong>关系操作</strong><small>删除操作会记录到版本历史和审计日志。</small></div>
|
|
6392
7136
|
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-relationship-delete>删除关系</button></div>
|
|
6393
7137
|
</section>` : "";
|
|
7138
|
+
setRelationshipPresence(item?.id ?? null);
|
|
6394
7139
|
openDialog(item ? "编辑人物关系" : "新建人物关系", field("from", "起点人物", "select", item?.fromCharacterId ?? defaultFrom, characterOptions) + field("to", "终点人物", "select", item?.toCharacterId ?? defaultTo, characterOptions) + field("category", "关系大类", "select", item?.category ?? "social", [["family", "亲属"], ["social", "社交"], ["emotional", "情感"], ["conflict", "冲突"], ["uncertain", "未确定"]]) + field("subtype", "关系子类", "text", item?.subtype) + field("keywords", "关系关键词", "keyword-chips", item?.keywords ?? []) + field("confidence", "置信度(0-1)", "number", item?.confidence ?? "1") + field("directed", "有方向性", "checkbox", item?.directed ?? false) + management, async (form) => {
|
|
6395
7140
|
const keywords = uniqueRelationshipKeywords(form.getAll("keywords").map(String));
|
|
6396
7141
|
await api(item ? `/api/relationships/${item.id}` : `/api/works/${state.work.id}/relationships`, { method: item ? "PATCH" : "POST", body: { fromCharacterId: form.get("from"), toCharacterId: form.get("to"), category: form.get("category"), subtype: form.get("subtype"), keywords, confidence: Number(form.get("confidence")), directed: form.get("directed") === "on", confirmationStatus: item?.confirmationStatus ?? "confirmed", ...(item ? { expectedVersionNo: item.versionNo } : {}) } });
|
|
@@ -6398,15 +7143,19 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
6398
7143
|
}, item ? "关系档案" : "人工确认关系");
|
|
6399
7144
|
$("#dialog-fields").querySelector("[data-dialog-relationship-delete]")?.addEventListener("click", async () => {
|
|
6400
7145
|
const dialog = $("#form-dialog");
|
|
7146
|
+
const reopenDialog = () => {
|
|
7147
|
+
setRelationshipPresence(item.id);
|
|
7148
|
+
dialog.showModal();
|
|
7149
|
+
};
|
|
6401
7150
|
dialog.close();
|
|
6402
|
-
if (!await confirmToast(`确认删除人物关系“${relationshipName}”吗?`, { title: "删除人物关系", confirmLabel: "继续删除" })) return
|
|
6403
|
-
if (!await confirmToast(`删除人物关系“${relationshipName}”后无法恢复。`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return
|
|
7151
|
+
if (!await confirmToast(`确认删除人物关系“${relationshipName}”吗?`, { title: "删除人物关系", confirmLabel: "继续删除" })) return reopenDialog();
|
|
7152
|
+
if (!await confirmToast(`删除人物关系“${relationshipName}”后无法恢复。`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return reopenDialog();
|
|
6404
7153
|
try {
|
|
6405
7154
|
await api(`/api/relationships/${item.id}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
6406
7155
|
await Promise.all([refreshRelationshipSurfaces(options.characterId ?? null), loadAiReferences()]);
|
|
6407
7156
|
toast(`已删除人物关系“${relationshipName}”`);
|
|
6408
7157
|
} catch (error) {
|
|
6409
|
-
|
|
7158
|
+
reopenDialog();
|
|
6410
7159
|
toast(error.message, "error");
|
|
6411
7160
|
}
|
|
6412
7161
|
});
|
|
@@ -6471,30 +7220,76 @@ async function openTaskDialog() {
|
|
|
6471
7220
|
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
6472
7221
|
${relationshipCharacterPicker}
|
|
6473
7222
|
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
|
|
6474
|
-
<div class="relationship-overwrite-card
|
|
7223
|
+
<div class="relationship-overwrite-card relationship-prefilter-card">
|
|
7224
|
+
<label class="checkbox-field"><input name="preFilterRelationshipSources" type="checkbox" checked disabled><span>分析前按人物名称和拼音过滤来源</span></label>
|
|
7225
|
+
<p>仅对定向人物分析生效。取消勾选后,将跳过前置过滤,无差别发送所选范围内的全部章节和设定来源。</p>
|
|
7226
|
+
</div>
|
|
7227
|
+
<div class="relationship-overwrite-card relationship-change-preview-card">
|
|
7228
|
+
<label class="checkbox-field"><input name="previewRelationshipChanges" type="checkbox" checked><span>分析完成后先预览关系变更</span></label>
|
|
7229
|
+
<p>建议保持开启。分析只生成新增、更新和删除清单,确认应用前不会修改人物关系库;取消勾选则沿用直接写入逻辑。</p>
|
|
7230
|
+
</div>
|
|
7231
|
+
<section class="relationship-source-preview-card" aria-labelledby="relationship-source-preview-title">
|
|
7232
|
+
<div class="relationship-source-preview-header">
|
|
7233
|
+
<div><strong id="relationship-source-preview-title">发送来源预检</strong><p>创建任务前查看将发送的章节与设定,并可取消误命中的来源。疑似拼音写法可能调用当前任务模型确认身份。</p></div>
|
|
7234
|
+
<button type="button" class="ghost-button" data-relationship-source-preview disabled>预览来源</button>
|
|
7235
|
+
</div>
|
|
7236
|
+
<div class="relationship-source-preview-content" data-relationship-source-preview-content role="status" aria-live="polite">选择至少一个被分析角色后即可预览。</div>
|
|
7237
|
+
</section>
|
|
7238
|
+
<div class="relationship-overwrite-card relationship-existing-overwrite-card hidden">
|
|
6475
7239
|
<label class="checkbox-field"><input name="replaceExistingRelationships" type="checkbox" disabled><span>用本次结果覆盖所选角色的已有关系</span></label>
|
|
6476
7240
|
<p>勾选后,任务成功时会先删除所有涉及所选角色的旧关系,再写入本次分析结果;不勾选则只追加新关系。</p>
|
|
6477
7241
|
</div>
|
|
6478
7242
|
<label>额外分析提示<textarea name="additionalPrompt" maxlength="10000" placeholder="例如:重点识别权力继承、师承变化或隐秘亲缘关系"></textarea><small>将同时追加到证据收集和全局关系归纳提示词,仅影响本次任务。</small></label>
|
|
6479
7243
|
</div>`;
|
|
6480
|
-
|
|
7244
|
+
let relationshipSourcePreview = null;
|
|
7245
|
+
let relationshipSourcePreviewConfigKey = "";
|
|
7246
|
+
const buildRelationshipScope = (form) => {
|
|
6481
7247
|
const taskType = String(form.get("taskType"));
|
|
6482
|
-
const modelId = String(form.get("modelId"));
|
|
6483
7248
|
const scopeType = String(form.get("scopeType"));
|
|
6484
7249
|
const includeAllSettings = taskType === "relationship-analysis" && scopeType === "book-with-settings";
|
|
6485
7250
|
const settingsOnly = taskType === "relationship-analysis" && scopeType === "settings";
|
|
6486
7251
|
const additionalPrompt = taskType === "relationship-analysis" ? String(form.get("additionalPrompt") ?? "").trim() : "";
|
|
6487
7252
|
const characterIds = taskType === "relationship-analysis" ? form.getAll("characterIds").map(String).filter(Boolean) : [];
|
|
7253
|
+
const preFilterRelationshipSources = characterIds.length > 0 && form.get("preFilterRelationshipSources") === "on";
|
|
7254
|
+
const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
|
|
6488
7255
|
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
6489
7256
|
const scope = settingsOnly
|
|
6490
|
-
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
7257
|
+
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6491
7258
|
: taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
|
|
6492
|
-
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6493
|
-
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
7259
|
+
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
7260
|
+
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
7261
|
+
return scope;
|
|
7262
|
+
};
|
|
7263
|
+
const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
|
|
7264
|
+
type: scope.type,
|
|
7265
|
+
chapterId: scope.chapterId ?? null,
|
|
7266
|
+
includeAllSettings: scope.includeAllSettings === true,
|
|
7267
|
+
characterIds: scope.characterIds ?? [],
|
|
7268
|
+
preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
|
|
7269
|
+
modelId
|
|
7270
|
+
});
|
|
7271
|
+
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
7272
|
+
const taskType = String(form.get("taskType"));
|
|
7273
|
+
const modelId = String(form.get("modelId"));
|
|
7274
|
+
const scope = buildRelationshipScope(form);
|
|
7275
|
+
if (taskType === "relationship-analysis" && relationshipSourcePreview
|
|
7276
|
+
&& relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
|
|
7277
|
+
scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
|
|
7278
|
+
.map((input) => ({
|
|
7279
|
+
sourceType: input.dataset.sourceType,
|
|
7280
|
+
sourceId: input.dataset.sourceId,
|
|
7281
|
+
sourceVersion: input.dataset.sourceVersion
|
|
7282
|
+
}));
|
|
7283
|
+
}
|
|
6494
7284
|
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
|
|
7285
|
+
await refreshBackgroundTaskCenter({ announce: false });
|
|
6495
7286
|
taskListPage = 1;
|
|
6496
|
-
|
|
6497
|
-
|
|
7287
|
+
try {
|
|
7288
|
+
await renderTasks(1);
|
|
7289
|
+
toast("分析任务已创建,已进入任务队列");
|
|
7290
|
+
} catch (error) {
|
|
7291
|
+
toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
|
|
7292
|
+
}
|
|
6498
7293
|
}, "AI 分析", {
|
|
6499
7294
|
submitLabel: "创建任务",
|
|
6500
7295
|
pendingLabel: "创建中…",
|
|
@@ -6518,8 +7313,11 @@ async function openTaskDialog() {
|
|
|
6518
7313
|
const relationshipCharacterCount = relationshipOptions.querySelector("[data-relationship-character-count]");
|
|
6519
7314
|
const relationshipCharacterClear = relationshipOptions.querySelector("[data-relationship-character-clear]");
|
|
6520
7315
|
const relationshipCharacterEmpty = relationshipOptions.querySelector("[data-relationship-character-empty]");
|
|
7316
|
+
const preFilterRelationships = relationshipOptions.querySelector('input[name="preFilterRelationshipSources"]');
|
|
7317
|
+
const relationshipSourcePreviewButton = relationshipOptions.querySelector("[data-relationship-source-preview]");
|
|
7318
|
+
const relationshipSourcePreviewContent = relationshipOptions.querySelector("[data-relationship-source-preview-content]");
|
|
6521
7319
|
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
6522
|
-
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-overwrite-card");
|
|
7320
|
+
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-existing-overwrite-card");
|
|
6523
7321
|
const allSettingsOption = document.createElement("option");
|
|
6524
7322
|
allSettingsOption.value = "book-with-settings";
|
|
6525
7323
|
allSettingsOption.textContent = "全书 + 设定集";
|
|
@@ -6547,6 +7345,41 @@ async function openTaskDialog() {
|
|
|
6547
7345
|
relationshipCharacterClear.disabled = selected.length === 0;
|
|
6548
7346
|
relationshipCharacterTrigger.setAttribute("aria-label", `筛选被分析角色,已选择 ${selected.length} 个`);
|
|
6549
7347
|
};
|
|
7348
|
+
const invalidateRelationshipSourcePreview = () => {
|
|
7349
|
+
relationshipSourcePreview = null;
|
|
7350
|
+
relationshipSourcePreviewConfigKey = "";
|
|
7351
|
+
relationshipSourcePreviewContent.classList.remove("is-loading");
|
|
7352
|
+
relationshipSourcePreviewContent.textContent = relationshipCharacterInputs.some((input) => input.checked)
|
|
7353
|
+
? "分析配置已变化,请重新预览将发送的来源。"
|
|
7354
|
+
: "选择至少一个被分析角色后即可预览。";
|
|
7355
|
+
};
|
|
7356
|
+
const relationshipSourceTypeLabel = (sourceType) => ({
|
|
7357
|
+
chapter: "章节",
|
|
7358
|
+
work: "作品资料",
|
|
7359
|
+
setting: "设定",
|
|
7360
|
+
character: "人物档案",
|
|
7361
|
+
race: "种族",
|
|
7362
|
+
organization: "组织",
|
|
7363
|
+
"timeline-track": "时间轴",
|
|
7364
|
+
"timeline-event": "时间线事件",
|
|
7365
|
+
relationship: "已有关系",
|
|
7366
|
+
"chapter-outline": "章节大纲",
|
|
7367
|
+
foreshadow: "伏笔",
|
|
7368
|
+
review: "审核项"
|
|
7369
|
+
}[sourceType] ?? sourceType);
|
|
7370
|
+
const renderRelationshipSourcePreview = (preview) => {
|
|
7371
|
+
const summary = `<div class="relationship-source-preview-summary"><strong>${preview.sourceCount} 条来源</strong><span>${preview.chapterCount} 章</span><span>${preview.settingCount} 条设定资料</span><span>约 ${Number(preview.totalCharacters).toLocaleString("zh-CN")} 字符</span><span>预计 ${preview.estimatedBatchCount} 个批次</span></div>`;
|
|
7372
|
+
if (!preview.sources.length) {
|
|
7373
|
+
relationshipSourcePreviewContent.innerHTML = `${summary}<p class="relationship-source-preview-empty">当前配置没有命中任何来源。仍可创建任务,但任务不会向模型发送正文或设定。</p>`;
|
|
7374
|
+
return;
|
|
7375
|
+
}
|
|
7376
|
+
const rows = preview.sources.map((source) => `<label class="relationship-source-preview-row">
|
|
7377
|
+
<input type="checkbox" data-relationship-source-selected data-source-type="${esc(source.sourceType)}" data-source-id="${esc(source.sourceId)}" data-source-version="${esc(source.version)}" checked>
|
|
7378
|
+
<span class="relationship-source-preview-main"><strong>${esc(source.title)}</strong><small>${esc(relationshipSourceTypeLabel(source.sourceType))} · ${Number(source.characterCount).toLocaleString("zh-CN")} 字符</small></span>
|
|
7379
|
+
<span class="relationship-source-preview-match ${source.matchType === "fuzzy" ? "is-fuzzy" : ""}">${source.matchType === "exact" ? "名称命中" : source.matchType === "fuzzy" ? "疑似写法确认" : "范围内来源"}</span>
|
|
7380
|
+
</label>`).join("");
|
|
7381
|
+
relationshipSourcePreviewContent.innerHTML = `${summary}<div class="relationship-source-preview-list" role="group" aria-label="选择要发送的来源">${rows}</div>`;
|
|
7382
|
+
};
|
|
6550
7383
|
const filterRelationshipCharacters = () => {
|
|
6551
7384
|
const query = relationshipCharacterSearch.value.trim().toLocaleLowerCase();
|
|
6552
7385
|
let visibleCount = 0;
|
|
@@ -6577,6 +7410,8 @@ async function openTaskDialog() {
|
|
|
6577
7410
|
for (const input of relationshipCharacterInputs) input.disabled = !enabled;
|
|
6578
7411
|
if (!enabled) setRelationshipCharacterBubbleOpen(false);
|
|
6579
7412
|
const hasSelectedCharacters = enabled && relationshipCharacterInputs.some((input) => input.checked);
|
|
7413
|
+
preFilterRelationships.disabled = !hasSelectedCharacters;
|
|
7414
|
+
relationshipSourcePreviewButton.disabled = !hasSelectedCharacters || !taskModelSelect.value;
|
|
6580
7415
|
replaceRelationships.disabled = !hasSelectedCharacters;
|
|
6581
7416
|
relationshipOverwriteCard.classList.toggle("hidden", !hasSelectedCharacters);
|
|
6582
7417
|
if (!hasSelectedCharacters) replaceRelationships.checked = false;
|
|
@@ -6591,17 +7426,56 @@ async function openTaskDialog() {
|
|
|
6591
7426
|
taskTypeSelect.addEventListener("change", () => {
|
|
6592
7427
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
6593
7428
|
syncTaskModelDefault();
|
|
7429
|
+
invalidateRelationshipSourcePreview();
|
|
7430
|
+
syncRelationshipOptions();
|
|
7431
|
+
});
|
|
7432
|
+
taskModelSelect.addEventListener("change", () => {
|
|
7433
|
+
invalidateRelationshipSourcePreview();
|
|
6594
7434
|
syncRelationshipOptions();
|
|
6595
7435
|
});
|
|
6596
7436
|
relationshipCharacterTrigger.addEventListener("click", () => {
|
|
6597
7437
|
setRelationshipCharacterBubbleOpen(relationshipCharacterTrigger.getAttribute("aria-expanded") !== "true");
|
|
6598
7438
|
});
|
|
6599
7439
|
relationshipCharacterSearch.addEventListener("input", filterRelationshipCharacters);
|
|
6600
|
-
for (const input of relationshipCharacterInputs) input.addEventListener("change",
|
|
7440
|
+
for (const input of relationshipCharacterInputs) input.addEventListener("change", () => {
|
|
7441
|
+
invalidateRelationshipSourcePreview();
|
|
7442
|
+
syncRelationshipOptions();
|
|
7443
|
+
});
|
|
6601
7444
|
relationshipCharacterClear.addEventListener("click", () => {
|
|
6602
7445
|
for (const input of relationshipCharacterInputs) input.checked = false;
|
|
7446
|
+
invalidateRelationshipSourcePreview();
|
|
6603
7447
|
syncRelationshipOptions();
|
|
6604
7448
|
});
|
|
7449
|
+
preFilterRelationships.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
7450
|
+
relationshipSourcePreviewButton.addEventListener("click", async () => {
|
|
7451
|
+
const form = new FormData($("#dynamic-form"));
|
|
7452
|
+
const scope = buildRelationshipScope(form);
|
|
7453
|
+
const modelId = String(form.get("modelId") ?? "");
|
|
7454
|
+
if (!scope.characterIds?.length) return toast("请先选择至少一个被分析角色", "error");
|
|
7455
|
+
relationshipSourcePreviewButton.disabled = true;
|
|
7456
|
+
relationshipSourcePreviewButton.textContent = "预检中…";
|
|
7457
|
+
relationshipSourcePreviewContent.classList.add("is-loading");
|
|
7458
|
+
relationshipSourcePreviewContent.textContent = "正在构建索引并核对来源,请稍候。";
|
|
7459
|
+
try {
|
|
7460
|
+
const preview = await api(`/api/works/${state.work.id}/tasks/relationship-source-preview`, {
|
|
7461
|
+
method: "POST",
|
|
7462
|
+
body: { scope, modelId }
|
|
7463
|
+
});
|
|
7464
|
+
relationshipSourcePreview = preview;
|
|
7465
|
+
relationshipSourcePreviewConfigKey = relationshipPreviewKey(scope, modelId);
|
|
7466
|
+
relationshipSourcePreviewContent.classList.remove("is-loading");
|
|
7467
|
+
renderRelationshipSourcePreview(preview);
|
|
7468
|
+
} catch (error) {
|
|
7469
|
+
relationshipSourcePreview = null;
|
|
7470
|
+
relationshipSourcePreviewConfigKey = "";
|
|
7471
|
+
relationshipSourcePreviewContent.classList.remove("is-loading");
|
|
7472
|
+
relationshipSourcePreviewContent.textContent = `来源预检失败:${error.message}`;
|
|
7473
|
+
toast(`来源预检失败:${error.message}`, "error");
|
|
7474
|
+
} finally {
|
|
7475
|
+
relationshipSourcePreviewButton.textContent = "重新预览";
|
|
7476
|
+
syncRelationshipOptions();
|
|
7477
|
+
}
|
|
7478
|
+
});
|
|
6605
7479
|
$("#dynamic-form").onclick = (event) => {
|
|
6606
7480
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
6607
7481
|
};
|
|
@@ -6611,7 +7485,11 @@ async function openTaskDialog() {
|
|
|
6611
7485
|
setRelationshipCharacterBubbleOpen(false);
|
|
6612
7486
|
relationshipCharacterTrigger.focus();
|
|
6613
7487
|
};
|
|
6614
|
-
scopeTypeSelect.addEventListener("change",
|
|
7488
|
+
scopeTypeSelect.addEventListener("change", () => {
|
|
7489
|
+
invalidateRelationshipSourcePreview();
|
|
7490
|
+
syncChapterField();
|
|
7491
|
+
});
|
|
7492
|
+
chapterSelect.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
6615
7493
|
syncRelationshipOptions();
|
|
6616
7494
|
}
|
|
6617
7495
|
|
|
@@ -7049,17 +7927,72 @@ async function openImportHistory() {
|
|
|
7049
7927
|
|
|
7050
7928
|
async function showChapterInsight() {
|
|
7051
7929
|
if (!state.chapter) return;
|
|
7052
|
-
const
|
|
7053
|
-
|
|
7930
|
+
const chapterId = state.chapter.id;
|
|
7931
|
+
dismissChapterInsightToast();
|
|
7932
|
+
const requestId = ++chapterInsightRequestId;
|
|
7933
|
+
const insights = await api(`/api/chapters/${chapterId}/insights`);
|
|
7934
|
+
if (requestId !== chapterInsightRequestId || state.chapter?.id !== chapterId || $("#editor-view").classList.contains("hidden")) return;
|
|
7054
7935
|
const insight = insights.find((item) => item.chapterVersion === state.chapter.versionNo) ?? insights[0];
|
|
7055
|
-
|
|
7936
|
+
const region = $("#toast-region");
|
|
7937
|
+
const element = document.createElement("section");
|
|
7938
|
+
element.id = "chapter-insight-toast";
|
|
7939
|
+
element.className = "toast chapter-insight-toast";
|
|
7940
|
+
element.setAttribute("role", "region");
|
|
7941
|
+
element.setAttribute("aria-label", "章节概览");
|
|
7942
|
+
const header = document.createElement("header");
|
|
7943
|
+
header.className = "chapter-insight-toast-header";
|
|
7944
|
+
const heading = document.createElement("div");
|
|
7945
|
+
heading.className = "chapter-insight-toast-heading";
|
|
7946
|
+
const title = document.createElement("strong");
|
|
7947
|
+
title.textContent = "章节概览";
|
|
7948
|
+
heading.append(title);
|
|
7949
|
+
if (insight && insight.chapterVersion !== state.chapter.versionNo) {
|
|
7950
|
+
const stale = document.createElement("span");
|
|
7951
|
+
stale.textContent = `基于旧版本 v${insight.chapterVersion}`;
|
|
7952
|
+
heading.append(stale);
|
|
7953
|
+
}
|
|
7954
|
+
const close = document.createElement("button");
|
|
7955
|
+
close.className = "ghost-button chapter-insight-toast-close";
|
|
7956
|
+
close.type = "button";
|
|
7957
|
+
close.textContent = "关闭";
|
|
7958
|
+
close.setAttribute("aria-label", "关闭章节概览");
|
|
7959
|
+
header.append(heading, close);
|
|
7960
|
+
const body = document.createElement("div");
|
|
7961
|
+
body.className = "chapter-insight-toast-body";
|
|
7056
7962
|
if (!insight) {
|
|
7057
|
-
|
|
7058
|
-
|
|
7963
|
+
const empty = document.createElement("p");
|
|
7964
|
+
empty.className = "chapter-insight-toast-empty";
|
|
7965
|
+
empty.textContent = "尚无章节概览。请在“AI 分析”中运行章节理解,完成后可在此查看结果。";
|
|
7966
|
+
body.append(empty);
|
|
7967
|
+
} else {
|
|
7968
|
+
const summary = document.createElement("p");
|
|
7969
|
+
summary.textContent = insight.summary || "暂无梗概";
|
|
7970
|
+
body.append(summary);
|
|
7971
|
+
const eventNames = insight.events.map((event) => typeof event === "string" ? event : (event.name ?? event.description ?? "未命名事件"));
|
|
7972
|
+
const uncertainties = insight.uncertainties.map((item) => typeof item === "string" ? item : JSON.stringify(item));
|
|
7973
|
+
[["事件", eventNames], ["待确认", uncertainties]].forEach(([label, items]) => {
|
|
7974
|
+
if (!items.length) return;
|
|
7975
|
+
const detail = document.createElement("div");
|
|
7976
|
+
detail.className = "chapter-insight-toast-detail";
|
|
7977
|
+
const detailLabel = document.createElement("strong");
|
|
7978
|
+
detailLabel.textContent = label;
|
|
7979
|
+
const detailContent = document.createElement("p");
|
|
7980
|
+
detailContent.textContent = items.join(";");
|
|
7981
|
+
detail.append(detailLabel, detailContent);
|
|
7982
|
+
body.append(detail);
|
|
7983
|
+
});
|
|
7059
7984
|
}
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7985
|
+
element.append(header, body);
|
|
7986
|
+
close.addEventListener("click", dismissChapterInsightToast, { once: true });
|
|
7987
|
+
element.addEventListener("keydown", (event) => {
|
|
7988
|
+
if (event.key !== "Escape") return;
|
|
7989
|
+
event.preventDefault();
|
|
7990
|
+
dismissChapterInsightToast();
|
|
7991
|
+
$("#insight-button").focus();
|
|
7992
|
+
});
|
|
7993
|
+
region.append(element);
|
|
7994
|
+
$("#insight-button").setAttribute("aria-expanded", "true");
|
|
7995
|
+
raiseToastRegion();
|
|
7063
7996
|
}
|
|
7064
7997
|
|
|
7065
7998
|
$("#home-button").addEventListener("click", async () => {
|
|
@@ -7606,6 +8539,20 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
7606
8539
|
$("#settings-return").addEventListener("click", () => returnFromSettings().catch((error) => toast(error.message, "error")));
|
|
7607
8540
|
$("#platform-ai-button").addEventListener("click", () => showPlatformAi().catch((error) => toast(error.message, "error")));
|
|
7608
8541
|
$("#platform-ai-return").addEventListener("click", () => returnToSettingsHub("#platform-ai-button").catch((error) => toast(error.message, "error")));
|
|
8542
|
+
$("#platform-usage-button").addEventListener("click", () => showPlatformUsage().catch((error) => toast(error.message, "error")));
|
|
8543
|
+
$("#platform-usage-return").addEventListener("click", () => returnToSettingsHub("#platform-usage-button").catch((error) => toast(error.message, "error")));
|
|
8544
|
+
$("#platform-usage-refresh").addEventListener("click", async () => {
|
|
8545
|
+
const button = $("#platform-usage-refresh");
|
|
8546
|
+
button.disabled = true;
|
|
8547
|
+
try {
|
|
8548
|
+
await renderPlatformTokenUsage();
|
|
8549
|
+
toast("Token 用量已刷新");
|
|
8550
|
+
} catch (error) {
|
|
8551
|
+
toast(error.message, "error");
|
|
8552
|
+
} finally {
|
|
8553
|
+
button.disabled = false;
|
|
8554
|
+
}
|
|
8555
|
+
});
|
|
7609
8556
|
$("#user-management-button").addEventListener("click", openUsersDialog);
|
|
7610
8557
|
$("#platform-ui-settings-button").addEventListener("click", openPlatformUiSettingsDialog);
|
|
7611
8558
|
$("#collaboration-button").addEventListener("click", () => openMembersDialog());
|
|
@@ -7629,7 +8576,14 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
7629
8576
|
body: {
|
|
7630
8577
|
toastPosition: $("#toast-position").value,
|
|
7631
8578
|
pageSizes: {
|
|
8579
|
+
settings: Number($("#page-size-settings").value),
|
|
7632
8580
|
characters: Number($("#page-size-characters").value),
|
|
8581
|
+
races: Number($("#page-size-races").value),
|
|
8582
|
+
organizations: Number($("#page-size-organizations").value),
|
|
8583
|
+
timeline: Number($("#page-size-timeline").value),
|
|
8584
|
+
outlines: Number($("#page-size-outlines").value),
|
|
8585
|
+
relationships: Number($("#page-size-relationships").value),
|
|
8586
|
+
reviews: Number($("#page-size-reviews").value),
|
|
7633
8587
|
analysisTasks: Number($("#page-size-analysis-tasks").value),
|
|
7634
8588
|
fileVersions: Number($("#page-size-file-versions").value)
|
|
7635
8589
|
}
|
|
@@ -7638,6 +8592,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
7638
8592
|
applyPlatformUiSettings(settings);
|
|
7639
8593
|
characterListPage = 1;
|
|
7640
8594
|
taskListPage = 1;
|
|
8595
|
+
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
7641
8596
|
$("#platform-ui-settings-dialog").close();
|
|
7642
8597
|
toast("界面与分页设置已保存");
|
|
7643
8598
|
} catch (error) {
|
|
@@ -7653,6 +8608,9 @@ $("#members-dialog").addEventListener("close", () => {
|
|
|
7653
8608
|
memberDialogMembers = [];
|
|
7654
8609
|
memberDialogDirectory = [];
|
|
7655
8610
|
});
|
|
8611
|
+
$("#form-dialog").addEventListener("close", () => {
|
|
8612
|
+
if (relationshipPresenceId && !$("#form-dialog").open) setRelationshipPresence(null);
|
|
8613
|
+
});
|
|
7656
8614
|
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
7657
8615
|
$("#member-permission-form").querySelectorAll("[data-permission-preset]").forEach((button) => button.addEventListener("click", () => {
|
|
7658
8616
|
$("#member-permission-grid").querySelectorAll("[data-member-permission]").forEach((select) => {
|
|
@@ -8059,6 +9017,26 @@ $(".quick-actions").addEventListener("click", (event) => {
|
|
|
8059
9017
|
$("#top-search-button").addEventListener("click", () => {
|
|
8060
9018
|
openSearchDialog().catch((error) => toast(error.message, "error"));
|
|
8061
9019
|
});
|
|
9020
|
+
$("#background-task-button").addEventListener("click", () => {
|
|
9021
|
+
renderBackgroundTaskCenter();
|
|
9022
|
+
const dialog = $("#background-task-dialog");
|
|
9023
|
+
if (!dialog.open) dialog.showModal();
|
|
9024
|
+
void refreshBackgroundTaskCenter();
|
|
9025
|
+
});
|
|
9026
|
+
$("#background-task-dialog").addEventListener("close", scheduleBackgroundTaskCenterRefresh);
|
|
9027
|
+
$("#background-task-refresh").addEventListener("click", async () => {
|
|
9028
|
+
const button = $("#background-task-refresh");
|
|
9029
|
+
button.disabled = true;
|
|
9030
|
+
try {
|
|
9031
|
+
await refreshBackgroundTaskCenter();
|
|
9032
|
+
} finally {
|
|
9033
|
+
button.disabled = false;
|
|
9034
|
+
}
|
|
9035
|
+
});
|
|
9036
|
+
$("#background-task-open-analysis").addEventListener("click", () => {
|
|
9037
|
+
$("#background-task-dialog").close();
|
|
9038
|
+
showModule("tasks").catch((error) => toast(error.message, "error"));
|
|
9039
|
+
});
|
|
8062
9040
|
$("#search-dialog-close").addEventListener("click", () => $("#search-dialog").close());
|
|
8063
9041
|
$("#search-form").addEventListener("submit", async (event) => {
|
|
8064
9042
|
event.preventDefault();
|